Compare commits
110 commits
e0b2e0a236
...
365d608d78
| Author | SHA1 | Date | |
|---|---|---|---|
| 365d608d78 | |||
| 973e659a24 | |||
| 4ed76a8a11 | |||
| c384c10a88 | |||
| f16ae2991e | |||
| 72fa2b3904 | |||
| a7b225b9df | |||
| 8a2a28e4be | |||
| e341ea6beb | |||
| 69c9ba1d8d | |||
| 50ff2a1c7a | |||
| fece9cc4a5 | |||
| 9203e71494 | |||
| 36a7d8e9d3 | |||
| ae675d6451 | |||
| c850cfb0db | |||
| 07b5553369 | |||
| 572b4ea167 | |||
| 0fb2f048f7 | |||
| 0dd7135781 | |||
| 89362f473f | |||
| 943d9b2a0c | |||
| d4090f983a | |||
| 94806782e1 | |||
| 1be73506cb | |||
| 2572e8c319 | |||
| f4bcb1c9cf | |||
| 6de814a247 | |||
| 2a00d69ebb | |||
| 3d196088c2 | |||
| c7f309b867 | |||
| 31950811c0 | |||
| fff3c29876 | |||
| f96f894168 | |||
| e73b6df40a | |||
| d4970a109d | |||
| b4ad9f8092 | |||
| 4bab6644b0 | |||
| c6a2a8cea3 | |||
| 8e90d7be94 | |||
| cae068c4e9 | |||
| d979ccefe6 | |||
| 68a783b7bb | |||
| ca206cf24e | |||
| 28ac875585 | |||
| c682e463d3 | |||
| f9d66bf53e | |||
| 8ab02dffa8 | |||
| 90f07a215c | |||
| b0b79efceb | |||
| d6e442f02b | |||
| a2c060639a | |||
| c16e9c243f | |||
| 46a5d15aa4 | |||
| 043b81a350 | |||
| a5601451cf | |||
| ce31eb09c4 | |||
| 4f5f8b0ee6 | |||
| 0b1c5c789d | |||
| dcde50f56a | |||
| 6565b25473 | |||
| 378227d708 | |||
| 36890cc432 | |||
| d99e31d587 | |||
| a60489d0cf | |||
| 8cf643131d | |||
| 54857e19c6 | |||
| 7b8e6600fe | |||
| e8af55a989 | |||
| 03be1f7348 | |||
| 0677b82dfc | |||
| 5f6b9a22b9 | |||
| 2e7713fec0 | |||
| 827a92e77e | |||
| 6a00090d0c | |||
| 6360938660 | |||
| 77f9dde4bf | |||
| 5ece0f15ca | |||
| ba29d13a17 | |||
| efebd7e4e2 | |||
| 044c6da31a | |||
| 91c431f23c | |||
| 35ea32dcea | |||
| acf7c0d152 | |||
| 633a3d6c72 | |||
| 36bf664eaa | |||
| 7217b6ab18 | |||
| eb5e0adc0f | |||
| 0782f99c19 | |||
| d588ade59d | |||
| ec51c095d8 | |||
| e8a6f2bdbd | |||
| f0bf1b0b62 | |||
| 64c86df9ac | |||
| bf34051069 | |||
| ac441a0a25 | |||
| d664bee36d | |||
| ee0e22adf5 | |||
| f457071d5d | |||
| 35843f740c | |||
| 001b919870 | |||
| 759fcf5cee | |||
| 15298c4101 | |||
| 28d90fd8d0 | |||
| 464c7db123 | |||
| 51d993d14b | |||
| 323389d13a | |||
| e90a561b4b | |||
|
|
38b5364823 | ||
| 31c391bbf3 |
354 changed files with 12228 additions and 4268 deletions
350
.drone.yml
350
.drone.yml
|
|
@ -22,55 +22,19 @@ steps:
|
|||
- yarn config set network-timeout 100000
|
||||
- yarn --cwd web install
|
||||
- tar --transform="s@.@./happydomain-${DRONE_COMMIT}@" --exclude-vcs --exclude=./web/node_modules/.cache -czf /dev/shm/happydomain-src.tar.gz .
|
||||
- mv /dev/shm/happydomain-src.tar.gz .
|
||||
- mkdir deploy
|
||||
- mv /dev/shm/happydomain-src.tar.gz deploy
|
||||
- yarn --cwd web --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-base/client.gen.ts
|
||||
- yarn --cwd web --offline build
|
||||
- yarn --cwd web-admin --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-admin/client.gen.ts
|
||||
- yarn --cwd web-admin --offline build
|
||||
|
||||
- name: deploy sources
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-src.tar.gz
|
||||
target: /${DRONE_BRANCH//\//-}/
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
|
||||
- name: deploy sources for release
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-src.tar.gz
|
||||
target: /${DRONE_TAG}/
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: backend-commit
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
|
@ -82,14 +46,48 @@ steps:
|
|||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: generate SBOM
|
||||
image: nemunaire/drone-syft
|
||||
settings:
|
||||
select_catalogers: go,npm
|
||||
output: spdx-json=deploy/happydomain-sbom.spdx.json
|
||||
source_name: happyDomain
|
||||
|
||||
- name: build-commit macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: build-tag macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: deploy
|
||||
image: plugins/s3
|
||||
settings:
|
||||
|
|
@ -101,8 +99,9 @@ steps:
|
|||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
source: deploy/*
|
||||
target: /${DRONE_BRANCH//\//-}/
|
||||
strip_prefix: deploy/
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
|
|
@ -121,72 +120,9 @@ steps:
|
|||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
target: /${DRONE_TAG}/
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: build-commit macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: build-tag macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: deploy macOS
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
target: /${DRONE_BRANCH//\//-}/
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
|
||||
- name: deploy macOS release
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
source: deploy/*
|
||||
target: /${DRONE_TAG}/
|
||||
strip_prefix: deploy/
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
|
@ -203,6 +139,54 @@ steps:
|
|||
password:
|
||||
from_secret: docker_password
|
||||
|
||||
- name: publish release on gitea
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: git_nemunaire_token
|
||||
base_url: https://git.nemunai.re
|
||||
draft: true
|
||||
files:
|
||||
- happydomain-src.tar.gz
|
||||
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
- happydomain-sbom.spdx.json
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish release on codeberg
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: codeberg_token
|
||||
base_url: https://codeberg.org
|
||||
draft: true
|
||||
files:
|
||||
- happydomain-src.tar.gz
|
||||
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
- happydomain-sbom.spdx.json
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish release on github
|
||||
image: plugins/github-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: github_release_token
|
||||
draft: true
|
||||
github_url: https://github.com
|
||||
files:
|
||||
- happydomain-src.tar.gz
|
||||
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
- happydomain-sbom.spdx.json
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
exclude:
|
||||
|
|
@ -246,8 +230,8 @@ steps:
|
|||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
|
@ -259,8 +243,8 @@ steps:
|
|||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
|
@ -276,6 +260,33 @@ steps:
|
|||
environment:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: build-commit macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: build-tag macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: deploy
|
||||
image: plugins/s3
|
||||
settings:
|
||||
|
|
@ -287,8 +298,9 @@ steps:
|
|||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
source: deploy/*
|
||||
target: /${DRONE_BRANCH//\//-}/
|
||||
strip_prefix: deploy/
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
|
|
@ -307,72 +319,9 @@ steps:
|
|||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
target: /${DRONE_TAG}/
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: build-commit macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: build-tag macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: deploy macOS
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
target: /${DRONE_BRANCH//\//-}/
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
|
||||
- name: deploy macOS release
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
source: deploy/*
|
||||
target: /${DRONE_TAG}/
|
||||
strip_prefix: deploy/
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
|
@ -389,6 +338,51 @@ steps:
|
|||
password:
|
||||
from_secret: docker_password
|
||||
|
||||
- name: publish release on gitea
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: git_nemunaire_token
|
||||
base_url: https://git.nemunai.re
|
||||
draft: true
|
||||
prerelease: true
|
||||
files:
|
||||
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish release on codeberg
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: codeberg_token
|
||||
base_url: https://codeberg.org
|
||||
draft: true
|
||||
prerelease: true
|
||||
files:
|
||||
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish release on github
|
||||
image: plugins/github-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: github_release_token
|
||||
draft: true
|
||||
prerelease: true
|
||||
github_url: https://github.com
|
||||
files:
|
||||
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- cron
|
||||
|
|
|
|||
83
README.md
83
README.md
|
|
@ -3,21 +3,42 @@ happyDomain
|
|||
|
||||
happyDomain is a free web application that centralizes the management of your domain names from different registrars and hosts.
|
||||
|
||||
[](./LICENSE)
|
||||
[](https://hub.docker.com/r/happydomain/happydomain)
|
||||
[](https://drone.nemunai.re/happyDomain/happyDomain)
|
||||
[](https://matrix.to/#/%23happyDNS:matrix.org)
|
||||
[](https://feedback.happydomain.org/)
|
||||
|
||||

|
||||
|
||||
It consists of a HTTP REST API written in Golang (primarily based on https://stackexchange.github.io/dnscontrol/ and https://github.com/miekg/dns) with a nice web interface written with [Svelte](https://svelte.dev/).
|
||||
It runs as a single stateless Linux binary, backed by a database (currently: LevelDB, more to come soon).
|
||||
It consists of a HTTP REST API written in Golang (primarily based on https://dnscontrol.org/ and https://github.com/miekg/dns) with a nice web interface written with [Svelte](https://svelte.dev/).
|
||||
It runs as a single stateless Linux binary, backed by a database.
|
||||
|
||||
**Features:**
|
||||
|
||||
Table of Contents
|
||||
-----------------
|
||||
|
||||
- [Features](#features)
|
||||
- [Getting Started With Docker](#getting-started-with-docker)
|
||||
- [Install from binary](#install-from-binary)
|
||||
- [Configuration](#use-happydomain)
|
||||
- [Building from source](#building)
|
||||
- [Development environment](#development-environment)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* An ultra fast web interface without compromise
|
||||
* Multiple domains management
|
||||
* Support for 44+ DNS providers (including dynamic DNS, RFC 2136) thanks to [DNSControl](https://stackexchange.github.io/dnscontrol/)
|
||||
* Support for 60+ DNS providers (including dynamic DNS, RFC 2136) thanks to [DNSControl](https://dnscontrol.org/)
|
||||
* Support for the most recents resource records thanks to [CoreDNS's library](https://github.com/miekg/dns)
|
||||
* Zone editor with a diff view to review the changes before propagation
|
||||
* Keep an history of published changes
|
||||
* Contextual help
|
||||
* Multiple users with authentication or one user without authtication
|
||||
* Multiple users with authentication or one user without authentication
|
||||
* Compatible with external authentication (OpenId Connect or through JWT tokens: Auth0, ...)
|
||||
|
||||
**happyDomain is functional but still very much a work in progress: it's a carefully crafted proof of concept that evolves thanks to you!**
|
||||
|
|
@ -27,8 +48,8 @@ Given the diversity of DNS configurations and user needs, we haven't yet identif
|
|||
[Whether it works for you or not, we need your feedback!](https://feedback.happydomain.org/) What do you think of our approach to simplifying domain name management? Your impressions at this stage help us guide the project according to **your actual expectations**.
|
||||
|
||||
|
||||
Using Docker
|
||||
------------
|
||||
Getting Started With Docker
|
||||
---------------------------
|
||||
|
||||
We are a Docker sponsored OSS project! Thus you can easily try and/or deploy our app using Docker/podman/kubernetes/...
|
||||
|
||||
|
|
@ -258,3 +279,51 @@ cd web; npm run dev
|
|||
```
|
||||
|
||||
With this setup, static assets integrated inside the go binary will not be used, instead it'll forward all requests for static assets to the node server, that do dynamic reload, etc.
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Contributions are welcome! Here's how you can help:
|
||||
|
||||
- **Report bugs:** Open an issue on your favorite forge: [GitHub](https://github.com/happyDomain/happydomain/issues), [Gitlab](https://gitlab.com/happyDomain/happydomain/-/issues), [Framagit](https://framagit.org/happyDomain/happydomain/-/issues), [Codeberg](https://codeberg.org/happyDomain/happyDomain/issues), we're highly responsive.
|
||||
- **Share feedback:** [Tell us what you think](https://feedback.happydomain.org/), your input guides the project.
|
||||
|
||||
|
||||
AI Disclaimer
|
||||
-------------
|
||||
|
||||
There have been questions about AI usage in project development. Our project handles domain name management, a sensitive area where mistakes can cause real outages, it's important to explain how AI is used in the development process.
|
||||
|
||||
AI is used as a helper for:
|
||||
|
||||
- verification of code quality and searching for vulnerabilities
|
||||
- cleaning up and improving documentation, comments and code
|
||||
- assistance during development
|
||||
- double-checking PRs and commits after human review
|
||||
|
||||
AI is not used for:
|
||||
|
||||
- writing entire features or components
|
||||
- "vibe coding" approach
|
||||
- code without line-by-line verification by a human
|
||||
- code without tests
|
||||
|
||||
The project has:
|
||||
|
||||
- CI/CD pipeline automation with tests and linting to ensure code quality
|
||||
- verification by experienced developers
|
||||
|
||||
So AI is just an assistant and a tool for developers to increase productivity and ensure code quality. The work is done by developers.
|
||||
|
||||
We do not differentiate between bad human code and AI vibe code. There are strict requirements for any code to be merged to keep the codebase maintainable. Even if code is written manually by a human, it's not guaranteed to be merged. Vibe code is not allowed and such PRs are rejected.
|
||||
|
||||
*Inspired by the [Databasus AI disclaimer](https://github.com/databasus/databasus#ai-disclaimer).*
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
happyDomain is licensed under the [GNU Affero General Public License v3.0](./LICENSE) (AGPL-3.0).
|
||||
|
||||
A commercial license is also available, contact us if interested.
|
||||
|
|
|
|||
264
README.zh-cn.md
Normal file
264
README.zh-cn.md
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
happyDomain
|
||||
===========
|
||||
|
||||
> 中文译者:[Exyone](https://www.exyone.me/)
|
||||
|
||||
由于软件架构限制,当前 happyDomain 仅支持简体中文,繁体中文用户请安装浏览器拓展插件以进行简繁转换。造成困扰,敬请谅解。
|
||||
|
||||
happyDomain 是一款免费的 Web 应用,可集中管理来自不同注册商和托管商的域名。
|
||||
|
||||

|
||||
|
||||
它由 Golang 编写的 HTTP REST API(主要基于 https://stackexchange.github.io/dnscontrol/ 和 https://github.com/miekg/dns)与 [Svelte](https://svelte.dev/) 构建的精美 Web 界面组成。
|
||||
作为单一无状态的 Linux 二进制文件运行,支持多种数据库(当前支持 LevelDB,更多选项即将推出)。
|
||||
|
||||
**主要特性:**
|
||||
|
||||
* 高性能 Web 界面,响应迅速
|
||||
* 支持多域名管理
|
||||
* 支持 60+ DNS 提供商(含动态 DNS、RFC 2136),得益于 [DNSControl](https://stackexchange.github.io/dnscontrol/)
|
||||
* 支持最新资源记录类型,得益于 [CoreDNS 库](https://github.com/miekg/dns)
|
||||
* 区域编辑器支持差异对比,部署前轻松审查变更
|
||||
* 保留部署变更历史记录
|
||||
* 上下文帮助
|
||||
* 支持多用户认证或单用户无认证模式
|
||||
* 兼容外部认证(OpenId Connect 或 JWT 令牌:Auth0 等)
|
||||
|
||||
**happyDomain 已可投入使用,但仍需不断完善:这是一个精心打造的概念验证版本,您的反馈将助力其不断进化!**
|
||||
|
||||
鉴于 DNS 配置和用户需求的多样性,我们尚未发现所有潜在问题。**若遇问题,请勿离去:[向我们反馈问题所在](https://github.com/happyDomain/happydomain/issues)。** 我们响应迅速,每个报告的 bug 都能帮助改进工具,惠及众人。
|
||||
|
||||
[无论使用体验如何,我们都期待您的反馈!](https://feedback.happydomain.org/) 您如何看待我们简化域名管理的方式?您的初步印象有助于我们根据**您的实际期望**来指引项目方向。
|
||||
|
||||
|
||||
使用 Docker
|
||||
------------
|
||||
|
||||
我们是由 Docker 赞助的开源项目!因此您可以轻松使用 Docker/podman/kubernetes/... 来试用或部署应用。
|
||||
|
||||
使用 `docker compose` 启动 happyDomain:
|
||||
|
||||
```bash
|
||||
git clone https://framagit.org/happyDomain/happyDomain.git
|
||||
cd happyDomain
|
||||
docker compose up
|
||||
```
|
||||
|
||||
或直接使用 `docker run`:
|
||||
|
||||
```bash
|
||||
docker run -e HAPPYDOMAIN_NO_AUTH=1 -p 8081:8081 happydomain/happydomain
|
||||
```
|
||||
|
||||
此命令将在数秒内启动 happyDomain,用于评估测试(无认证、临时存储等)。使用浏览器访问 <http://localhost:8081> 即可体验!
|
||||
|
||||
部署 happyDomain,请查阅 [Docker 镜像文档](https://hub.docker.com/r/happydomain/happydomain)。
|
||||
|
||||
|
||||
从二进制文件安装
|
||||
-------------------
|
||||
|
||||
预编译二进制文件下载地址:<https://get.happydomain.org/>
|
||||
|
||||
选择目录(最新版本或 master 分支),然后选择与您的操作系统和 CPU 架构对应的二进制文件。
|
||||
|
||||
|
||||
使用 happyDomain
|
||||
---------------
|
||||
|
||||
二进制文件附带默认配置,可直接启动。在终端中运行以下命令即可:
|
||||
|
||||
```bash
|
||||
./happyDomain
|
||||
```
|
||||
|
||||
初始化完成后,应显示以下信息:
|
||||
|
||||
Admin listening on ./happydomain.sock
|
||||
Ready, listening on :8081
|
||||
|
||||
访问 http://localhost:8081/ 开始使用 happyDomain。
|
||||
|
||||
|
||||
### 数据库配置
|
||||
|
||||
默认使用 LevelDB 存储引擎。可使用 `-storage-engine` 选项更改存储引擎。
|
||||
|
||||
运行 `./happyDomain -help` 查看可用存储引擎:
|
||||
|
||||
```
|
||||
-storage-engine value
|
||||
在 [inmemory leveldb oracle-nosql postgresql] 中选择存储引擎 (默认 leveldb)
|
||||
```
|
||||
|
||||
#### LevelDB
|
||||
|
||||
LevelDB 是轻量级嵌入式键值存储(类似 SQLite,无需额外守护进程)。
|
||||
|
||||
```
|
||||
-leveldb-path string
|
||||
LevelDB 数据库路径 (默认 "happydomain.db")
|
||||
```
|
||||
|
||||
默认在二进制文件所在目录创建 `happydomain.db` 目录。可更改为更有意义或更持久的路径。
|
||||
|
||||
#### inmemory
|
||||
|
||||
数据存储于内存中,服务停止后数据即丢失。
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
PostgreSQL 支持主要面向已部署 PostgreSQL 数据库基础设施的环境。这允许您利用现有数据库设置、备份流程和运维工具,无需部署额外数据库系统。
|
||||
|
||||
happyDomain 以键值存储模式使用 PostgreSQL,将所有数据存储在包含 `key` 和 `value` 列的单张表中。虽然可行,但请注意,与专用键值存储相比,PostgreSQL 并非键值工作负载的最佳选择。若从头部署且需超出 LevelDB 的可扩展性,请考虑使用专为键值操作设计的存储后端。
|
||||
|
||||
```
|
||||
-postgres-database string
|
||||
PostgreSQL 数据库名称 (默认 "happydomain")
|
||||
-postgres-host string
|
||||
PostgreSQL 服务器主机名 (默认 "localhost")
|
||||
-postgres-password string
|
||||
PostgreSQL 密码
|
||||
-postgres-port int
|
||||
PostgreSQL 服务器端口 (默认 5432)
|
||||
-postgres-ssl-mode string
|
||||
PostgreSQL SSL 模式 (disable, require, verify-ca, verify-full) (默认 "disable")
|
||||
-postgres-table string
|
||||
键值存储的 PostgreSQL 表名 (默认 "happydomain_kv")
|
||||
-postgres-user string
|
||||
PostgreSQL 用户名 (默认 "happydomain")
|
||||
```
|
||||
|
||||
#### Oracle NoSQL Database
|
||||
|
||||
Oracle NoSQL Database 是来自 Oracle Cloud Infrastructure (OCI) 的全托管云服务,提供按需吞吐量和高可用的存储配置。happyDomain 可将其作为可扩展的云端存储后端用于生产部署。
|
||||
|
||||
使用 Oracle NoSQL Database 需拥有 OCI 账户并创建 NoSQL 表。表需包含主键字段 `key`(字符串类型)和 `value` 字段(JSON 类型)存储数据。认证使用 OCI 的 IAM 和 API 签名密钥。
|
||||
|
||||
配置以下选项连接 happyDomain 至 Oracle NoSQL Database:
|
||||
|
||||
```
|
||||
-oci-compartment string
|
||||
NoSQL 数据库所在的 OCI 隔间 ID
|
||||
-oci-fingerprint string
|
||||
OCI 用户 API 密钥指纹
|
||||
-oci-private-key-file string
|
||||
给定用户的 OCI 私钥文件路径
|
||||
-oci-region string
|
||||
NoSQL 数据库所在的 OCI 区域 (默认 "us-phoenix-1")
|
||||
-oci-table string
|
||||
存储值的表名 (默认 "happydomain")
|
||||
-oci-tenancy string
|
||||
NoSQL 数据库所在的 OCI 租户 ID
|
||||
-oci-user string
|
||||
访问 NoSQL 数据库的 OCI 用户 ID
|
||||
```
|
||||
|
||||
#### 数据库管理系统
|
||||
|
||||
MySQL/Mariadb 等 DBMS 已不再支持,亦无相关计划。
|
||||
|
||||
|
||||
持久化配置
|
||||
-------------------
|
||||
|
||||
二进制文件会自动查找以下配置文件:
|
||||
|
||||
* 当前目录下的 `./happydomain.conf`;
|
||||
* `$XDG_CONFIG_HOME/happydomain/happydomain.conf`;
|
||||
* `/etc/happydomain.conf`。
|
||||
|
||||
仅使用找到的第一个文件。
|
||||
|
||||
也可通过命令行参数指定自定义路径:
|
||||
|
||||
```sh
|
||||
./happyDomain /etc/happydomain/config
|
||||
```
|
||||
|
||||
#### 配置文件格式
|
||||
|
||||
注释行必须以 # 开头,不支持行尾注释。
|
||||
|
||||
每行放置配置选项名称和期望值,用 `=` 分隔。例如:
|
||||
|
||||
```
|
||||
storage-engine=leveldb
|
||||
leveldb-path=/var/lib/happydomain/db/
|
||||
```
|
||||
|
||||
#### 环境变量
|
||||
|
||||
还会查找以 `HAPPYDOMAIN_` 开头的特殊环境变量。
|
||||
|
||||
使用以下环境变量可达到与上述示例相同的效果:
|
||||
|
||||
```
|
||||
HAPPYDOMAIN_STORAGE_ENGINE=leveldb
|
||||
HAPPYDOMAIN_LEVELDB_PATH=/var/lib/happydomain/db/
|
||||
```
|
||||
|
||||
只需将短横线替换为下划线即可。
|
||||
|
||||
#### 需要 OVH API?
|
||||
|
||||
OVH 没有简单的 API 密钥或凭据,需通过 Web 流程获取密钥。
|
||||
|
||||
启动认证流程,happyDomain 实例需配备专用应用程序密钥。
|
||||
|
||||
[连接 OVH,请按以下说明操作](https://help.happydomain.org/en/introduction/deploy/ovh)。
|
||||
|
||||
|
||||
构建
|
||||
--------
|
||||
|
||||
### 依赖项
|
||||
|
||||
构建 happyDomain 项目需具备以下依赖项:
|
||||
|
||||
* `go`;
|
||||
* `nodejs`,已测试版本 22;
|
||||
* `swag`,已测试版本 1.16(可通过 `go install github.com/swaggo/swag/cmd/swag@latest` 安装)。
|
||||
|
||||
|
||||
### 构建步骤
|
||||
|
||||
1. 首先准备前端,安装 node 模块依赖:
|
||||
|
||||
```bash
|
||||
pushd web; npm install; popd
|
||||
```
|
||||
|
||||
2. 然后生成 Go 代码使用的资源文件:
|
||||
|
||||
```bash
|
||||
go generate -tags swagger,web ./...
|
||||
```
|
||||
|
||||
3. 最后编译 Go 代码:
|
||||
|
||||
```bash
|
||||
go build -tags swagger,web ./cmd/happyDomain
|
||||
```
|
||||
|
||||
此命令将创建独立二进制文件 `happyDomain`。
|
||||
|
||||
|
||||
开发环境
|
||||
-----------------------
|
||||
|
||||
若要为前端做贡献,而非每次修改后都重新生成前端资源(使用 `go generate`),可使用开发工具:
|
||||
|
||||
一个终端中使用以下参数运行 happydomain:
|
||||
|
||||
```bash
|
||||
./happyDomain -dev http://127.0.0.1:5173
|
||||
```
|
||||
|
||||
另一终端运行 node 部分:
|
||||
|
||||
```bash
|
||||
cd web; npm run dev
|
||||
```
|
||||
|
||||
此设置不使用集成到 go 二进制文件中的静态资源,而是将所有静态资源请求转发至 node 服务器,实现动态重载等功能。
|
||||
64
SECURITY.md
Normal file
64
SECURITY.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest version of happyDomain is supported with security fixes.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | --------- |
|
||||
| latest | ✓ |
|
||||
| < latest| ✗ |
|
||||
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- happyDomain application code (API/backend and web frontend)
|
||||
- Other websites directly operated by the happyDomain team: documentation, main website, blog, git redirection, downloads website, demo instance, insights
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Vulnerabilities in third-party dependencies that are not directly exploitable in happyDomain
|
||||
- Social engineering attacks
|
||||
- Denial-of-service attacks requiring significant resources
|
||||
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in happyDomain, please report it privately.
|
||||
|
||||
By email: security@happydomain.org
|
||||
On GitHub: https://github.com/happydomain/happydomain/security/advisories
|
||||
On Gitlab: https://gitlab.com/happyDomain/happyDomain/-/issues/new (check Confidential issue before submitting)
|
||||
On Framagit: https://framagit.org/happyDomain/happyDomain/-/issues/new (check Confidential issue before submitting)
|
||||
|
||||
Please include:
|
||||
- description of the vulnerability
|
||||
- steps to reproduce
|
||||
- potential impact
|
||||
|
||||
|
||||
## Disclosure policy
|
||||
|
||||
We follow a responsible disclosure process.
|
||||
|
||||
After receiving a report we will:
|
||||
1. acknowledge within 72 hours
|
||||
2. investigate the issue
|
||||
3. prepare a fix
|
||||
4. publish a security advisory when the fix is available
|
||||
|
||||
|
||||
## Safe Harbor
|
||||
|
||||
We consider security research conducted in good faith to be authorized. We will not pursue legal action against researchers who:
|
||||
- Report vulnerabilities through the channels listed above
|
||||
- Avoid accessing, modifying, or deleting data that doesn't belong to them
|
||||
- Avoid degrading the availability of our services
|
||||
- Do not publicly disclose the vulnerability before a fix is available
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
We are happy to credit security researchers who responsibly disclose vulnerabilities.
|
||||
61
checks/interface.go
Normal file
61
checks/interface.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package checks provides the registry for domain health checkers.
|
||||
// It allows individual checker implementations to self-register at startup
|
||||
// via init() functions and exposes functions to retrieve registered checkers.
|
||||
package checks // import "git.happydns.org/happyDomain/checks"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// checkersList is the ordered list of all registered checks.
|
||||
var checkersList map[string]happydns.Checker = map[string]happydns.Checker{}
|
||||
|
||||
// RegisterChecker declares the existence of the given check. It is intended to
|
||||
// be called from init() functions in individual check files so that each check
|
||||
// self-registers at program startup.
|
||||
//
|
||||
// If two checks try to register the same environment name the program will
|
||||
// terminate: name collisions are a configuration error, not a runtime one.
|
||||
func RegisterChecker(name string, checker happydns.Checker) {
|
||||
log.Println("Registering new checker:")
|
||||
checkersList[name] = checker
|
||||
}
|
||||
|
||||
// GetCheckers returns the ordered list of all registered checks.
|
||||
func GetCheckers() *map[string]happydns.Checker {
|
||||
return &checkersList
|
||||
}
|
||||
|
||||
// FindChecker returns the check registered under the given environment name,
|
||||
// or an error if no check with that name exists.
|
||||
func FindChecker(name string) (happydns.Checker, error) {
|
||||
c, ok := checkersList[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find check %q", name)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
|
@ -23,11 +23,9 @@ package main
|
|||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/earthboundkid/versioninfo/v2"
|
||||
"github.com/fatih/color"
|
||||
|
|
@ -63,7 +61,6 @@ func main() {
|
|||
}
|
||||
|
||||
log.Println("This is happyDomain", versioninfo.Short())
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
// Disabled colors in dnscontrol corrections
|
||||
color.NoColor = true
|
||||
|
|
@ -86,7 +83,7 @@ func main() {
|
|||
|
||||
var adminSrv *app.Admin
|
||||
if opts.AdminBind != "" {
|
||||
adminSrv := app.NewAdmin(a)
|
||||
adminSrv = app.NewAdmin(a)
|
||||
go adminSrv.Start()
|
||||
}
|
||||
|
||||
|
|
|
|||
129
go.mod
129
go.mod
|
|
@ -2,15 +2,16 @@ module git.happydns.org/happyDomain
|
|||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.26.0
|
||||
toolchain go1.26.1
|
||||
|
||||
require (
|
||||
github.com/StackExchange/dnscontrol/v4 v4.29.0
|
||||
github.com/StackExchange/dnscontrol/v4 v4.34.0
|
||||
github.com/altcha-org/altcha-lib-go v1.0.0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/earthboundkid/versioninfo/v2 v2.24.1
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/gin-contrib/sessions v1.0.4
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/go-mail/mail v2.3.1+incompatible
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
|
|
@ -26,15 +27,15 @@ require (
|
|||
github.com/swaggo/swag v1.16.6
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.18.1 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
codeberg.org/miekg/dns v0.6.40 // indirect
|
||||
codeberg.org/miekg/dns v0.6.67 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
|
|
@ -51,52 +52,52 @@ require (
|
|||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // indirect
|
||||
github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v12 v12.3.0 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
|
||||
github.com/altcha-org/altcha-lib-go v1.0.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/billputer/go-namecheap v0.0.0-20210108011502-994a912fb7f9 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 v4.0.7 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.18 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.116.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deepmap/oapi-codegen v1.16.3 // indirect
|
||||
github.com/digitalocean/godo v1.173.0 // indirect
|
||||
github.com/digitalocean/godo v1.176.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go/v8 v8.1.0 // indirect
|
||||
github.com/exoscale/egoscale v0.102.4 // indirect
|
||||
github.com/failsafe-go/failsafe-go v0.9.5 // indirect
|
||||
github.com/failsafe-go/failsafe-go v0.9.6 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/flosch/pongo2/v4 v4.0.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-gandi/go-gandi v0.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
|
|
@ -112,22 +113,23 @@ require (
|
|||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
|
|
@ -135,7 +137,7 @@ require (
|
|||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/terraform-plugin-log v0.10.0 // indirect
|
||||
github.com/hetznercloud/hcloud-go/v2 v2.36.0 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.186 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 // indirect
|
||||
github.com/influxdata/tdigest v0.0.1 // indirect
|
||||
github.com/iris-contrib/schema v0.0.6 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
|
|
@ -148,7 +150,7 @@ require (
|
|||
github.com/kataras/pio v0.0.14 // indirect
|
||||
github.com/kataras/sitemap v0.0.6 // indirect
|
||||
github.com/kataras/tunnel v0.0.4 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
|
|
@ -169,8 +171,7 @@ require (
|
|||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/oracle/oci-go-sdk/v65 v65.107.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/oracle/oci-go-sdk/v65 v65.109.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/peterhellberg/link v1.2.0 // indirect
|
||||
github.com/philhug/opensrs-go v0.0.0-20171126225031-9dfa7433020d // indirect
|
||||
|
|
@ -181,21 +182,21 @@ require (
|
|||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/procfs v0.20.0 // indirect
|
||||
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.58.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/softlayer/softlayer-go v1.2.1 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.24.8 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.5 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.24.9 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.8 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/transip/gotransip/v6 v6.26.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
|
|
@ -208,37 +209,39 @@ require (
|
|||
github.com/vultr/govultr/v2 v2.17.2 // indirect
|
||||
github.com/yosssi/ace v0.0.5 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/arch v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/api v0.264.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/api v0.269.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/grpc v1.79.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
gopkg.in/mail.v2 v2.3.1 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.16.0 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.17.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
moul.io/http2curl v1.0.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/StackExchange/dnscontrol/v4 => github.com/happyDomain/dnscontrol/v4 v4.33.100
|
||||
replace github.com/StackExchange/dnscontrol/v4 => github.com/happyDomain/dnscontrol/v4 v4.36.100
|
||||
|
||||
// https://github.com/kataras/iris/issues/2587
|
||||
replace github.com/kataras/golog v0.1.15 => github.com/kataras/golog v0.1.13
|
||||
|
|
|
|||
304
go.sum
304
go.sum
|
|
@ -1,12 +1,14 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
codeberg.org/miekg/dns v0.6.40 h1:dzO+f0pcQEejc5kQYhOV8pSdJ14uc640M21valACl4s=
|
||||
codeberg.org/miekg/dns v0.6.40/go.mod h1:fIxAzBMDPnXWSw0fp8+pfZMRiAqYY4+HHYLzUo/S6Dg=
|
||||
codeberg.org/miekg/dns v0.6.62 h1:3Uua303EC8Og75QqT+pGRrcvKNTOouehHOQS36KbSqc=
|
||||
codeberg.org/miekg/dns v0.6.62/go.mod h1:fIxAzBMDPnXWSw0fp8+pfZMRiAqYY4+HHYLzUo/S6Dg=
|
||||
codeberg.org/miekg/dns v0.6.67 h1:vsVNsqAOE9uYscJHIHNtoCxiEySQn/B9BEvAUYI5Zmc=
|
||||
codeberg.org/miekg/dns v0.6.67/go.mod h1:58Y3ZTg6Z5ZEm/ZAAwHehbZfrD4u5mE4RByHoPEMyKk=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
|
|
@ -58,8 +60,8 @@ github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi
|
|||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v12 v12.3.0 h1:iGVPe/gPqzpXggbbmVLWR0TyJ9UoPoqKL+kspjseZzE=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v12 v12.3.0/go.mod h1:76JtkiCKMwTdTOlKe9goT4Md+oWjfMouGBQgy+u1bgc=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
||||
github.com/altcha-org/altcha-lib-go v1.0.0 h1:7oPti0aUS+YCep8nwt5b9g4jYfCU55ZruWESL8G9K5M=
|
||||
|
|
@ -70,42 +72,79 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
|
|||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.15 h1:w+QfByC1CE+dkExfdIqNGVtyqGNE+uxbBCHNLafJ1/0=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.15/go.mod h1:gqNlsw/2sJb4sSyhwounZLf+lEAQN9USPoDbD7SbJEE=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.2 h1:zoD/SoiVQi8l8tuQn//VexrXS2yorg/+717JNA4Ble8=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.2/go.mod h1:Ll1DCasPTBFtHK5t/U5WIwGIyRuY3xY+x8/LmqIlqpM=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3 h1:JRPXnIr0WwFsSHBmuCvT/uh0Vgys+crvwkOghbJEqi8=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3/go.mod h1:DHddp7OO4bY467WVCqWBzk5+aEWn7vqYkap7UigJzGk=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.16 h1:k+TqYbG/WtL43wSCALuuPjLPEt//Ck/ZDKpCWrzhjUU=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.16/go.mod h1:yEr1gPPNbetOFxQV0J9ZLL5cR4U4ujEBgwk6p6oKYc8=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.17 h1:Fw2SIR63jhfLpFZr6955zU3g9V8ouHC/pRpmmiHmIFM=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.17/go.mod h1:x9PRRtbCQ/gv1ziQPXFB7nQwQgVLQ+FSvPIkVAhRcYY=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/billputer/go-namecheap v0.0.0-20210108011502-994a912fb7f9 h1:2vQTbEJvFsyd1VefzZ34GUkUD6TkJleYYJh9/25WBE4=
|
||||
|
|
@ -118,13 +157,11 @@ github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVk
|
|||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 v4.0.7 h1:Jk7uhY5q11fE5PlEupX2Lo12w82UhGC6bE1CI5jwFbc=
|
||||
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 v4.0.7/go.mod h1:FnQtD0+Q/1NZxi0eEWN+3ZRyMsE9vzSB3YjyunkbKD0=
|
||||
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.18 h1:RvyTDU0VmnUBd3Qm2i6irEXtCR2KRIxnRlD8l+5z/DY=
|
||||
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.18/go.mod h1:a6n4wXFHbMW0iJFxHIJR4PkgG5krP52nOVCBU0m+Obw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
|
|
@ -144,10 +181,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deepmap/oapi-codegen v1.16.3 h1:GT9G86SbQtT1r8ZB+4Cybi9VGdu1P5ieNvNdEoCSbrA=
|
||||
github.com/deepmap/oapi-codegen v1.16.3/go.mod h1:JD6ErqeX0nYnhdciLc61Konj3NBASREMlkHOgHn8WAM=
|
||||
github.com/digitalocean/godo v1.173.0 h1:tgzevGhlz9VFjk2y3NmeItUT4vIVVCRFETlG/1GlEQI=
|
||||
github.com/digitalocean/godo v1.173.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
|
||||
github.com/digitalocean/godo v1.175.0 h1:tpfwJFkBzpePxvvFazOn69TXctdxuFlOs7DMVXsI7oU=
|
||||
github.com/digitalocean/godo v1.175.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
|
||||
github.com/digitalocean/godo v1.176.0 h1:P379vPO5TUre+bUHPEsdSAbl5vIrRRhP91tMIEPoWYU=
|
||||
github.com/digitalocean/godo v1.176.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY=
|
||||
github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8=
|
||||
github.com/dnsimple/dnsimple-go/v8 v8.1.0 h1:U4ENaNCe5aUFHLiF7lj2NNpLPzFY3YIriu/UzrdfUbg=
|
||||
github.com/dnsimple/dnsimple-go/v8 v8.1.0/go.mod h1:61MdYHRL+p2TBBUVEkxo1n4iRF6s3R9fZcvQvyt5du8=
|
||||
github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg=
|
||||
github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
|
|
@ -155,8 +196,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/exoscale/egoscale v0.102.4 h1:GBKsZMIOzwBfSu+4ZmWka3Ejf2JLiaBDHp4CQUgvp2E=
|
||||
github.com/exoscale/egoscale v0.102.4/go.mod h1:ROSmPtle0wvf91iLZb09++N/9BH2Jo9XxIpAEumvocA=
|
||||
github.com/failsafe-go/failsafe-go v0.9.5 h1:Bgt4wTKV3+n49GssB2njPZ4u5ApjvtKSIQlqIL4E3oo=
|
||||
github.com/failsafe-go/failsafe-go v0.9.5/go.mod h1:IeRpglkcwzKagjDMh90ZhN2l4Ovt3+jemQBUbThag54=
|
||||
github.com/failsafe-go/failsafe-go v0.9.6 h1:vPSH2cry0Ee5cnR9wc9qshCDO6jdrMA9elBJNwyo4Uk=
|
||||
github.com/failsafe-go/failsafe-go v0.9.6/go.mod h1:IeRpglkcwzKagjDMh90ZhN2l4Ovt3+jemQBUbThag54=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
|
|
@ -169,8 +210,8 @@ github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0H
|
|||
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
|
||||
|
|
@ -179,6 +220,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
|||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-gandi/go-gandi v0.7.0 h1:gsP33dUspsN1M+ZW9HEgHchK9HiaSkYnltO73RHhSZA=
|
||||
github.com/go-gandi/go-gandi v0.7.0/go.mod h1:9NoYyfWCjFosClPiWjkbbRK5UViaZ4ctpT8/pKSSFlw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
|
|
@ -218,6 +261,8 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxE
|
|||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
|
|
@ -232,8 +277,8 @@ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy0
|
|||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe h1:zn8tqiUbec4wR94o7Qj3LZCAT6uGobhEgnDRg6isG5U=
|
||||
github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
|
|
@ -266,8 +311,8 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
|
|||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
|
|
@ -277,7 +322,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
|
|
@ -287,14 +331,12 @@ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/Z
|
|||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
|
|
@ -309,8 +351,10 @@ github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/
|
|||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/happyDomain/dnscontrol/v4 v4.33.100 h1:n9hXu7/rG44ZOMBctwCwCatx1CKQdGjaIkwY2df6g4k=
|
||||
github.com/happyDomain/dnscontrol/v4 v4.33.100/go.mod h1:q+BnNrB7hMVY2yXgICG/Su/dumLW9HWc4+KVfDTW5YU=
|
||||
github.com/happyDomain/dnscontrol/v4 v4.35.101 h1:9GL4OZ05AXOBUNjRdU72UUJGFAQz6OTCRVQw3WTQuo8=
|
||||
github.com/happyDomain/dnscontrol/v4 v4.35.101/go.mod h1:R6j+Fv+etKriXI3runhnv42nPZPLcn81NNRt9gl1hTs=
|
||||
github.com/happyDomain/dnscontrol/v4 v4.36.100 h1:wrNaUV3Ihcqd9t9+AEIyBiyF1QNAeuFbCj+j8w0a/sM=
|
||||
github.com/happyDomain/dnscontrol/v4 v4.36.100/go.mod h1:7fgVrun0ecnT8fJhcFHQQXBg6yVIfEWRRQOj27hxm+s=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
|
|
@ -323,8 +367,8 @@ github.com/hetznercloud/hcloud-go/v2 v2.36.0 h1:HlLL/aaVXUulqe+rsjoJmrxKhPi1MflL
|
|||
github.com/hetznercloud/hcloud-go/v2 v2.36.0/go.mod h1:MnN/QJEa/RYNQiiVoJjNHPntM7Z1wlYPgJ2HA40/cDE=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.186 h1:8P/G6KfCsRPraIHAUFfhsfiZuOmuhMpL4jocRru1EYE=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.186/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 h1:J+U6+eUjIsBhefolFdZW5hQNJbkMj+7msxZrv56Cg2g=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
|
||||
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/influxdata/tdigest v0.0.1 h1:XpFptwYmnEKUqmkcDjrzffswZ3nvNeevbUSLPP/ZzIY=
|
||||
|
|
@ -365,13 +409,12 @@ github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwf
|
|||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
|
|
@ -430,7 +473,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||
|
|
@ -446,12 +488,12 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff
|
|||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||
github.com/oracle/nosql-go-sdk v1.4.7 h1:dqVBSMulObDj0JHm1mAncTHrQg8wIiQJNC0JRNKPACg=
|
||||
github.com/oracle/nosql-go-sdk v1.4.7/go.mod h1:xgJE9wxADDbk7vR4FGA4NOt4RNAaIsQOj4sCATmCVXM=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.107.0 h1:ZBnDn495o4beF+bidJuIDYubwEVypiOhtVrmIQd0kWY=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.107.0/go.mod h1:8ZzvzuEG/cFLFZhxg/Mg1w19KqyXBKO3c17QIc5PkGs=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.108.3 h1:n2G4QwGoRNhtLE8r24/+Ny+WpEMdc9ggGpnPvVYM2Yk=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.108.3/go.mod h1:8ZzvzuEG/cFLFZhxg/Mg1w19KqyXBKO3c17QIc5PkGs=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.109.0 h1:EsbFVvcV+uid9SoQnFQbTKS6FgqsM9NtoKmUIovKsbk=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.109.0/go.mod h1:8ZzvzuEG/cFLFZhxg/Mg1w19KqyXBKO3c17QIc5PkGs=
|
||||
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||
github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c=
|
||||
|
|
@ -476,12 +518,14 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
|
|||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
|
||||
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc=
|
||||
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
|
||||
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
|
|
@ -498,14 +542,11 @@ github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
|||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||
github.com/softlayer/softlayer-go v1.2.1 h1:8ucHxn5laVsVPb0/aMGnr6tOMt1I9BgEtU5mn70OGKw=
|
||||
|
|
@ -516,7 +557,6 @@ github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
|||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/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=
|
||||
|
|
@ -542,10 +582,10 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
|||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE=
|
||||
github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw=
|
||||
github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU=
|
||||
github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/minify/v2 v2.24.9 h1:W6A570F9N6MuZtg9mdHXD93piZZIWJaGpbAw9Narrfw=
|
||||
github.com/tdewolff/minify/v2 v2.24.9/go.mod h1:9F66jUzl/Pdf6Q5x0RXFUsI/8N1kjBb3ILg9ABSWoOI=
|
||||
github.com/tdewolff/parse/v2 v2.8.8 h1:l3yOJ4OUKq1sKeQQxZ7P2yZ6daW/Oq4IDxL98uTOpPI=
|
||||
github.com/tdewolff/parse/v2 v2.8.8/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
|
|
@ -601,34 +641,38 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
|||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
|
||||
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
|
||||
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
||||
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
|
@ -642,14 +686,16 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
|||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
|
|
@ -664,8 +710,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
@ -686,11 +732,15 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -703,12 +753,13 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -722,7 +773,6 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -734,6 +784,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
|
@ -757,6 +809,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
@ -765,7 +819,6 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
|
|||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
|
|
@ -774,8 +827,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
@ -788,24 +841,26 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E
|
|||
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
google.golang.org/api v0.264.0 h1:+Fo3DQXBK8gLdf8rFZ3uLu39JpOnhvzJrLMQSoSYZJM=
|
||||
google.golang.org/api v0.264.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
|
||||
google.golang.org/api v0.268.0 h1:hgA3aS4lt9rpF5RCCkX0Q2l7DvHgvlb53y4T4u6iKkA=
|
||||
google.golang.org/api v0.268.0/go.mod h1:HXMyMH496wz+dAJwD/GkAPLd3ZL33Kh0zEG32eNvy9w=
|
||||
google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg=
|
||||
google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
|
@ -823,16 +878,15 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
|||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
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/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.16.0 h1:mUczKFnrCystSV7yIODzVSbENoud3T7DwstmyVZfqg4=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.16.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.17.2 h1:x8YKHqCJWkC/hddfUhw7FRqTG0x3fr/0ZnWYN+i4THs=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.17.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
package adapter
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"strings"
|
||||
|
||||
dnscontrol "github.com/StackExchange/dnscontrol/v4/models"
|
||||
|
|
@ -88,9 +89,24 @@ func DNSControlDiffByRecord(oldrrs []happydns.Record, newrrs []happydns.Record,
|
|||
|
||||
ret := make([]*happydns.Correction, len(corrections))
|
||||
for i, correction := range corrections {
|
||||
id := sha256.Sum224([]byte(correction.MsgsJoined))
|
||||
|
||||
var oldRecords []happydns.Record
|
||||
for _, rc := range correction.Old {
|
||||
oldRecords = append(oldRecords, rc.ToRR())
|
||||
}
|
||||
|
||||
var newRecords []happydns.Record
|
||||
for _, rc := range correction.New {
|
||||
newRecords = append(newRecords, rc.ToRR())
|
||||
}
|
||||
|
||||
ret[i] = &happydns.Correction{
|
||||
Msg: correction.MsgsJoined,
|
||||
Kind: DNSControlFromCorrectionType(correction.Type),
|
||||
Id: id[:],
|
||||
Msg: correction.MsgsJoined,
|
||||
Kind: DNSControlFromCorrectionType(correction.Type),
|
||||
OldRecords: oldRecords,
|
||||
NewRecords: newRecords,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,3 +147,71 @@ func NewDNSControlDomainConfig(origin string, rrs []happydns.Record) (*dnscontro
|
|||
Records: records,
|
||||
}, err
|
||||
}
|
||||
|
||||
// recordKey returns a canonical string for matching records by name, type, and rdata.
|
||||
func recordKey(r happydns.Record) string {
|
||||
return r.String()
|
||||
}
|
||||
|
||||
// BuildTargetRecords computes the target record set by applying the selected
|
||||
// corrections to the provider's current records. It starts with a copy of
|
||||
// providerRecords, then for each correction whose ID is in selectedIDs:
|
||||
// - Addition: appends NewRecords
|
||||
// - Deletion: removes matching OldRecords
|
||||
// - Update: removes matching OldRecords, appends NewRecords
|
||||
func BuildTargetRecords(
|
||||
providerRecords []happydns.Record,
|
||||
corrections []*happydns.Correction,
|
||||
selectedIDs []happydns.Identifier,
|
||||
) []happydns.Record {
|
||||
// Build a set of selected IDs for fast lookup.
|
||||
selected := make(map[string]bool, len(selectedIDs))
|
||||
for _, id := range selectedIDs {
|
||||
selected[string(id)] = true
|
||||
}
|
||||
|
||||
// Start with a copy of provider records.
|
||||
result := make([]happydns.Record, len(providerRecords))
|
||||
copy(result, providerRecords)
|
||||
|
||||
for _, cr := range corrections {
|
||||
if !selected[string(cr.Id)] {
|
||||
continue
|
||||
}
|
||||
|
||||
switch cr.Kind {
|
||||
case happydns.CorrectionKindAddition:
|
||||
result = append(result, cr.NewRecords...)
|
||||
|
||||
case happydns.CorrectionKindDeletion:
|
||||
result = removeRecords(result, cr.OldRecords)
|
||||
|
||||
case happydns.CorrectionKindUpdate:
|
||||
result = removeRecords(result, cr.OldRecords)
|
||||
result = append(result, cr.NewRecords...)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// removeRecords removes records from the slice that match any of the toRemove
|
||||
// records by their canonical string representation. Each toRemove record
|
||||
// removes at most one match.
|
||||
func removeRecords(records []happydns.Record, toRemove []happydns.Record) []happydns.Record {
|
||||
removeKeys := make(map[string]int, len(toRemove))
|
||||
for _, r := range toRemove {
|
||||
removeKeys[recordKey(r)]++
|
||||
}
|
||||
|
||||
result := make([]happydns.Record, 0, len(records))
|
||||
for _, r := range records {
|
||||
key := recordKey(r)
|
||||
if removeKeys[key] > 0 {
|
||||
removeKeys[key]--
|
||||
continue
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
279
internal/adapters/dnscontrol-correction_test.go
Normal file
279
internal/adapters/dnscontrol-correction_test.go
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package adapter_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
adapter "git.happydns.org/happyDomain/internal/adapters"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func makeA(name string, ip string) happydns.Record {
|
||||
return &dns.A{
|
||||
Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||
A: net.ParseIP(ip),
|
||||
}
|
||||
}
|
||||
|
||||
func makeMX(name string, pref uint16, mx string) happydns.Record {
|
||||
return &dns.MX{
|
||||
Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 300},
|
||||
Preference: pref,
|
||||
Mx: mx,
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTargetRecords_AllSelected(t *testing.T) {
|
||||
providerRecords := []happydns.Record{
|
||||
makeA("example.com.", "1.2.3.4"),
|
||||
}
|
||||
|
||||
newRecord := makeA("example.com.", "5.6.7.8")
|
||||
corrections := []*happydns.Correction{
|
||||
{
|
||||
Id: happydns.Identifier([]byte("add-1")),
|
||||
Kind: happydns.CorrectionKindAddition,
|
||||
NewRecords: []happydns.Record{newRecord},
|
||||
},
|
||||
}
|
||||
|
||||
selectedIDs := []happydns.Identifier{
|
||||
happydns.Identifier([]byte("add-1")),
|
||||
}
|
||||
|
||||
result := adapter.BuildTargetRecords(providerRecords, corrections, selectedIDs)
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 records, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTargetRecords_NoneSelected(t *testing.T) {
|
||||
providerRecords := []happydns.Record{
|
||||
makeA("example.com.", "1.2.3.4"),
|
||||
}
|
||||
|
||||
corrections := []*happydns.Correction{
|
||||
{
|
||||
Id: happydns.Identifier([]byte("add-1")),
|
||||
Kind: happydns.CorrectionKindAddition,
|
||||
NewRecords: []happydns.Record{makeA("example.com.", "5.6.7.8")},
|
||||
},
|
||||
}
|
||||
|
||||
result := adapter.BuildTargetRecords(providerRecords, corrections, nil)
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 record, got %d", len(result))
|
||||
}
|
||||
if result[0].String() != providerRecords[0].String() {
|
||||
t.Errorf("expected unchanged provider record, got %s", result[0].String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTargetRecords_Deletion(t *testing.T) {
|
||||
providerRecords := []happydns.Record{
|
||||
makeA("example.com.", "1.2.3.4"),
|
||||
makeA("example.com.", "5.6.7.8"),
|
||||
}
|
||||
|
||||
corrections := []*happydns.Correction{
|
||||
{
|
||||
Id: happydns.Identifier([]byte("del-1")),
|
||||
Kind: happydns.CorrectionKindDeletion,
|
||||
OldRecords: []happydns.Record{makeA("example.com.", "1.2.3.4")},
|
||||
},
|
||||
}
|
||||
|
||||
selectedIDs := []happydns.Identifier{
|
||||
happydns.Identifier([]byte("del-1")),
|
||||
}
|
||||
|
||||
result := adapter.BuildTargetRecords(providerRecords, corrections, selectedIDs)
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 record, got %d", len(result))
|
||||
}
|
||||
if result[0].String() != providerRecords[1].String() {
|
||||
t.Errorf("expected remaining record %s, got %s", providerRecords[1].String(), result[0].String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTargetRecords_Update(t *testing.T) {
|
||||
oldRecord := makeA("example.com.", "1.2.3.4")
|
||||
newRecord := makeA("example.com.", "9.8.7.6")
|
||||
|
||||
providerRecords := []happydns.Record{oldRecord}
|
||||
|
||||
corrections := []*happydns.Correction{
|
||||
{
|
||||
Id: happydns.Identifier([]byte("upd-1")),
|
||||
Kind: happydns.CorrectionKindUpdate,
|
||||
OldRecords: []happydns.Record{oldRecord},
|
||||
NewRecords: []happydns.Record{newRecord},
|
||||
},
|
||||
}
|
||||
|
||||
selectedIDs := []happydns.Identifier{
|
||||
happydns.Identifier([]byte("upd-1")),
|
||||
}
|
||||
|
||||
result := adapter.BuildTargetRecords(providerRecords, corrections, selectedIDs)
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 record, got %d", len(result))
|
||||
}
|
||||
if result[0].String() != newRecord.String() {
|
||||
t.Errorf("expected updated record %s, got %s", newRecord.String(), result[0].String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTargetRecords_PartialSelection(t *testing.T) {
|
||||
providerRecords := []happydns.Record{
|
||||
makeA("example.com.", "1.2.3.4"),
|
||||
}
|
||||
|
||||
corrections := []*happydns.Correction{
|
||||
{
|
||||
Id: happydns.Identifier([]byte("add-1")),
|
||||
Kind: happydns.CorrectionKindAddition,
|
||||
NewRecords: []happydns.Record{makeA("example.com.", "5.6.7.8")},
|
||||
},
|
||||
{
|
||||
Id: happydns.Identifier([]byte("add-2")),
|
||||
Kind: happydns.CorrectionKindAddition,
|
||||
NewRecords: []happydns.Record{makeMX("example.com.", 10, "mail.example.com.")},
|
||||
},
|
||||
}
|
||||
|
||||
// Only select the first correction.
|
||||
selectedIDs := []happydns.Identifier{
|
||||
happydns.Identifier([]byte("add-1")),
|
||||
}
|
||||
|
||||
result := adapter.BuildTargetRecords(providerRecords, corrections, selectedIDs)
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 records, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTargetRecords_MixedOperations(t *testing.T) {
|
||||
providerRecords := []happydns.Record{
|
||||
makeA("example.com.", "1.2.3.4"),
|
||||
makeA("example.com.", "10.0.0.1"),
|
||||
}
|
||||
|
||||
corrections := []*happydns.Correction{
|
||||
{
|
||||
Id: happydns.Identifier([]byte("del-1")),
|
||||
Kind: happydns.CorrectionKindDeletion,
|
||||
OldRecords: []happydns.Record{makeA("example.com.", "10.0.0.1")},
|
||||
},
|
||||
{
|
||||
Id: happydns.Identifier([]byte("add-1")),
|
||||
Kind: happydns.CorrectionKindAddition,
|
||||
NewRecords: []happydns.Record{makeA("example.com.", "5.6.7.8")},
|
||||
},
|
||||
}
|
||||
|
||||
selectedIDs := []happydns.Identifier{
|
||||
happydns.Identifier([]byte("del-1")),
|
||||
happydns.Identifier([]byte("add-1")),
|
||||
}
|
||||
|
||||
result := adapter.BuildTargetRecords(providerRecords, corrections, selectedIDs)
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 records, got %d", len(result))
|
||||
}
|
||||
|
||||
// Should have 1.2.3.4 and 5.6.7.8 (10.0.0.1 deleted, 5.6.7.8 added)
|
||||
found := map[string]bool{}
|
||||
for _, r := range result {
|
||||
found[r.String()] = true
|
||||
}
|
||||
if !found[makeA("example.com.", "1.2.3.4").String()] {
|
||||
t.Error("expected record 1.2.3.4 to remain")
|
||||
}
|
||||
if !found[makeA("example.com.", "5.6.7.8").String()] {
|
||||
t.Error("expected record 5.6.7.8 to be added")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSControlDiffByRecord_EnrichedFields(t *testing.T) {
|
||||
oldRecords := []happydns.Record{
|
||||
makeA("example.com.", "1.2.3.4"),
|
||||
}
|
||||
|
||||
newRecords := []happydns.Record{
|
||||
makeA("example.com.", "1.2.3.4"),
|
||||
makeA("example.com.", "5.6.7.8"),
|
||||
}
|
||||
|
||||
corrections, nbDiffs, err := adapter.DNSControlDiffByRecord(oldRecords, newRecords, "example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if nbDiffs == 0 {
|
||||
t.Fatal("expected at least 1 diff")
|
||||
}
|
||||
|
||||
if len(corrections) == 0 {
|
||||
t.Fatal("expected at least 1 correction")
|
||||
}
|
||||
|
||||
for _, c := range corrections {
|
||||
if len(c.Id) == 0 {
|
||||
t.Error("expected correction to have an ID")
|
||||
}
|
||||
|
||||
switch c.Kind {
|
||||
case happydns.CorrectionKindAddition:
|
||||
if len(c.NewRecords) == 0 {
|
||||
t.Error("addition correction should have NewRecords")
|
||||
}
|
||||
case happydns.CorrectionKindDeletion:
|
||||
if len(c.OldRecords) == 0 {
|
||||
t.Error("deletion correction should have OldRecords")
|
||||
}
|
||||
case happydns.CorrectionKindUpdate:
|
||||
if len(c.OldRecords) == 0 || len(c.NewRecords) == 0 {
|
||||
t.Error("update correction should have both OldRecords and NewRecords")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSControlDiffByRecord_NoChanges(t *testing.T) {
|
||||
records := []happydns.Record{
|
||||
makeA("example.com.", "1.2.3.4"),
|
||||
}
|
||||
|
||||
corrections, _, err := adapter.DNSControlDiffByRecord(records, records, "example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(corrections) != 0 {
|
||||
t.Errorf("expected 0 corrections for identical zones, got %d", len(corrections))
|
||||
}
|
||||
}
|
||||
|
|
@ -210,7 +210,8 @@ func (ac *AuthUserController) DeleteAuthUser(c *gin.Context) {
|
|||
func (ac *AuthUserController) EmailValidationLink(c *gin.Context) {
|
||||
user := c.MustGet("authuser").(*happydns.UserAuth)
|
||||
|
||||
happydns.ApiResponse(c, ac.auService.GenerateValidationLink(user), nil)
|
||||
link, err := ac.auService.GenerateValidationLink(user)
|
||||
happydns.ApiResponse(c, link, err)
|
||||
}
|
||||
|
||||
// RecoverUserAcct generates an account recovery link for a user.
|
||||
|
|
|
|||
|
|
@ -83,18 +83,18 @@ func (bc *BackupController) DoBackup() (ret happydns.Backup) {
|
|||
|
||||
for _, dn := range ds {
|
||||
// Domain logs
|
||||
ls, err := bc.store.ListDomainLogs(dn)
|
||||
if err != nil {
|
||||
ret.Errors = append(ret.Errors, fmt.Sprintf("unable to retrieve domain's logs %s/%s (%s): %s", u.Id.String(), dn.Id.String(), dn.DomainName, err.Error()))
|
||||
ls, logErr := bc.store.ListDomainLogs(dn)
|
||||
if logErr != nil {
|
||||
ret.Errors = append(ret.Errors, fmt.Sprintf("unable to retrieve domain's logs %s/%s (%s): %s", u.Id.String(), dn.Id.String(), dn.DomainName, logErr.Error()))
|
||||
} else {
|
||||
ret.DomainsLogs[dn.Id.String()] = ls
|
||||
}
|
||||
|
||||
// Zones
|
||||
for _, zid := range dn.ZoneHistory {
|
||||
z, err := bc.store.GetZone(zid)
|
||||
if err != nil {
|
||||
ret.Errors = append(ret.Errors, fmt.Sprintf("unable to retrieve domain's zone %s/%s (%s): zoneid=%s: %s", u.Id.String(), dn.Id.String(), dn.DomainName, zid.String(), err.Error()))
|
||||
z, zoneErr := bc.store.GetZone(zid)
|
||||
if zoneErr != nil {
|
||||
ret.Errors = append(ret.Errors, fmt.Sprintf("unable to retrieve domain's zone %s/%s (%s): zoneid=%s: %s", u.Id.String(), dn.Id.String(), dn.DomainName, zid.String(), zoneErr.Error()))
|
||||
} else {
|
||||
ret.Zones = append(ret.Zones, z)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,12 @@ type DomainController struct {
|
|||
store domain.DomainStorage
|
||||
}
|
||||
|
||||
func NewDomainController(duService happydns.DomainUsecase, remoteZoneImporter happydns.RemoteZoneImporterUsecase, zoneImporter happydns.ZoneImporterUsecase, store domain.DomainStorage) *DomainController {
|
||||
func NewDomainController(
|
||||
duService happydns.DomainUsecase,
|
||||
remoteZoneImporter happydns.RemoteZoneImporterUsecase,
|
||||
zoneImporter happydns.ZoneImporterUsecase,
|
||||
store domain.DomainStorage,
|
||||
) *DomainController {
|
||||
return &DomainController{
|
||||
duService,
|
||||
remoteZoneImporter,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,12 @@ type ZoneController struct {
|
|||
store zone.ZoneStorage
|
||||
}
|
||||
|
||||
func NewZoneController(domainService happydns.DomainUsecase, zoneService happydns.ZoneUsecase, zoneCorrectionService happydns.ZoneCorrectionApplierUsecase, store zone.ZoneStorage) *ZoneController {
|
||||
func NewZoneController(
|
||||
domainService happydns.DomainUsecase,
|
||||
zoneService happydns.ZoneUsecase,
|
||||
zoneCorrectionService happydns.ZoneCorrectionApplierUsecase,
|
||||
store zone.ZoneStorage,
|
||||
) *ZoneController {
|
||||
return &ZoneController{
|
||||
domainService,
|
||||
zoneService,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,12 @@ import (
|
|||
"git.happydns.org/happyDomain/internal/storage"
|
||||
)
|
||||
|
||||
func declareZoneServiceRoutes(apiZonesRoutes *gin.RouterGroup, zc *controller.ZoneController, dep Dependencies, store storage.Storage) {
|
||||
func declareZoneServiceRoutes(
|
||||
apiZonesRoutes *gin.RouterGroup,
|
||||
zc *controller.ZoneController,
|
||||
dep Dependencies,
|
||||
store storage.Storage,
|
||||
) {
|
||||
sc := controller.NewServiceController(
|
||||
dep.Service,
|
||||
dep.ZoneService,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ func NewLoginController(authService happydns.AuthenticationUsecase, captchaVerif
|
|||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} happydns.User
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /auth/user [get]
|
||||
// @Router /auth [get]
|
||||
func (lc *LoginController) GetLoggedUser(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, c.MustGet("LoggedUser"))
|
||||
}
|
||||
|
|
@ -86,11 +86,11 @@ func (lc *LoginController) Login(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Check if captcha is required for this IP/email combination
|
||||
if lc.captcha.Provider() != "" {
|
||||
requiresCaptcha := lc.failureTracker.RequiresCaptcha(c.ClientIP(), request.Email)
|
||||
|
||||
if requiresCaptcha {
|
||||
// Enforce captcha when a provider is configured and the failure threshold
|
||||
// is reached. Failure tracking runs unconditionally so it stays effective
|
||||
// even on deployments without a captcha provider.
|
||||
if lc.failureTracker.RequiresCaptcha(c.ClientIP(), request.Email) {
|
||||
if lc.captcha.Provider() != "" {
|
||||
if request.CaptchaToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, happydns.LoginErrorResponse{
|
||||
Message: "Captcha verification required.",
|
||||
|
|
@ -99,7 +99,7 @@ func (lc *LoginController) Login(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := lc.captcha.Verify(request.CaptchaToken, c.ClientIP()); err != nil {
|
||||
if err = lc.captcha.Verify(request.CaptchaToken, c.ClientIP()); err != nil {
|
||||
log.Printf("%s: captcha verification failed: %s", c.ClientIP(), err.Error())
|
||||
c.JSON(http.StatusUnauthorized, happydns.LoginErrorResponse{
|
||||
Message: "Captcha verification failed.",
|
||||
|
|
@ -107,31 +107,41 @@ func (lc *LoginController) Login(c *gin.Context) {
|
|||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// No captcha provider — signal a plain rate-limit to the client.
|
||||
c.JSON(http.StatusTooManyRequests, happydns.LoginErrorResponse{
|
||||
Message: "Too many failed login attempts. Please wait before trying again.",
|
||||
RateLimited: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, err := lc.authService.AuthenticateUserWithPassword(request)
|
||||
if err != nil {
|
||||
log.Printf("%s: %s", c.ClientIP(), err.Error())
|
||||
log.Printf("%s %s: %s", c.ClientIP(), request.Email, err.Error())
|
||||
|
||||
if lc.captcha.Provider() != "" {
|
||||
lc.failureTracker.RecordFailure(c.ClientIP(), request.Email)
|
||||
if lc.failureTracker.RequiresCaptcha(c.ClientIP(), request.Email) {
|
||||
lc.failureTracker.RecordFailure(c.ClientIP(), request.Email)
|
||||
if lc.failureTracker.RequiresCaptcha(c.ClientIP(), request.Email) {
|
||||
if lc.captcha.Provider() != "" {
|
||||
c.JSON(http.StatusUnauthorized, happydns.LoginErrorResponse{
|
||||
Message: "Invalid username or password.",
|
||||
CaptchaRequired: true,
|
||||
})
|
||||
return
|
||||
} else {
|
||||
c.JSON(http.StatusTooManyRequests, happydns.LoginErrorResponse{
|
||||
Message: "Too many failed login attempts. Please wait before trying again.",
|
||||
RateLimited: true,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusUnauthorized, happydns.LoginErrorResponse{Message: "Invalid username or password."})
|
||||
return
|
||||
}
|
||||
|
||||
if lc.captcha.Provider() != "" {
|
||||
lc.failureTracker.RecordSuccess(c.ClientIP(), request.Email)
|
||||
}
|
||||
lc.failureTracker.RecordSuccess(c.ClientIP(), request.Email)
|
||||
|
||||
middleware.SessionLoginOK(c, user)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,9 +26,8 @@ package controller
|
|||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
|
@ -45,6 +44,9 @@ import (
|
|||
|
||||
const (
|
||||
SESSION_KEY_OIDC_STATE = "oidc-state"
|
||||
SESSION_KEY_OIDC_PKCE = "oidc-pkce"
|
||||
SESSION_KEY_OIDC_NONCE = "oidc-nonce"
|
||||
SESSION_KEY_OIDC_NEXT = "oidc-next"
|
||||
)
|
||||
|
||||
type OIDCProvider struct {
|
||||
|
|
@ -94,6 +96,12 @@ func NewOIDCProvider(cfg *happydns.Options, authService happydns.AuthenticationU
|
|||
func (p *OIDCProvider) RedirectOIDC(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Capture and validate the post-login redirect destination.
|
||||
// Only accept same-origin relative paths to prevent open redirect.
|
||||
if next := c.Query("next"); next != "" && strings.HasPrefix(next, "/") && !strings.HasPrefix(next, "//") {
|
||||
session.Set(SESSION_KEY_OIDC_NEXT, next)
|
||||
}
|
||||
|
||||
state := make([]byte, 32)
|
||||
_, err := rand.Read(state)
|
||||
if err != nil {
|
||||
|
|
@ -102,7 +110,19 @@ func (p *OIDCProvider) RedirectOIDC(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
nonce := make([]byte, 32)
|
||||
if _, err = rand.Read(nonce); err != nil {
|
||||
log.Println("Unable to redirect_OIDC, rand.Read fails:", err)
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: "Sorry, we are currently unable to respond to your request. Please retry later."})
|
||||
return
|
||||
}
|
||||
nonceStr := hex.EncodeToString(nonce)
|
||||
|
||||
pkceVerifier := oauth2.GenerateVerifier()
|
||||
|
||||
session.Set(SESSION_KEY_OIDC_STATE, hex.EncodeToString(state))
|
||||
session.Set(SESSION_KEY_OIDC_PKCE, pkceVerifier)
|
||||
session.Set(SESSION_KEY_OIDC_NONCE, nonceStr)
|
||||
err = session.Save()
|
||||
if err != nil {
|
||||
log.Println("Unable to redirect_OIDC, session.Save fails:", err)
|
||||
|
|
@ -110,7 +130,7 @@ func (p *OIDCProvider) RedirectOIDC(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, p.oauth2config.AuthCodeURL(hex.EncodeToString(state)))
|
||||
c.Redirect(http.StatusFound, p.oauth2config.AuthCodeURL(hex.EncodeToString(state), oauth2.S256ChallengeOption(pkceVerifier), oauth2.SetAuthURLParam("nonce", nonceStr)))
|
||||
}
|
||||
|
||||
func (p *OIDCProvider) CompleteOIDC(c *gin.Context) {
|
||||
|
|
@ -124,34 +144,51 @@ func (p *OIDCProvider) CompleteOIDC(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
session.Delete(SESSION_KEY_OIDC_STATE)
|
||||
err := session.Save()
|
||||
if err != nil {
|
||||
log.Println("Unable to CompleteOIDC, session.Save fails:", err)
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: "Sorry, we are currently unable to respond to your request. Please retry later."})
|
||||
return
|
||||
}
|
||||
pkceVerifier, _ := session.Get(SESSION_KEY_OIDC_PKCE).(string)
|
||||
expectedNonce, _ := session.Get(SESSION_KEY_OIDC_NONCE).(string)
|
||||
nextPath, _ := session.Get(SESSION_KEY_OIDC_NEXT).(string)
|
||||
|
||||
oauth2Token, err := p.oauth2config.Exchange(c, c.Query("code"))
|
||||
// Consume the OIDC session keys in-memory now. The actual session.Save()
|
||||
// is deferred to SessionLoginOK (via session.Clear + new session). On any
|
||||
// error below the in-memory changes are discarded, preserving the session
|
||||
// keys so the user can retry without restarting the whole flow. The
|
||||
// authorization code itself is single-use at the provider level, so
|
||||
// replaying the callback with the same code is rejected there.
|
||||
session.Delete(SESSION_KEY_OIDC_STATE)
|
||||
session.Delete(SESSION_KEY_OIDC_PKCE)
|
||||
session.Delete(SESSION_KEY_OIDC_NONCE)
|
||||
session.Delete(SESSION_KEY_OIDC_NEXT)
|
||||
|
||||
oauth2Token, err := p.oauth2config.Exchange(c, c.Query("code"), oauth2.VerifierOption(pkceVerifier))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: fmt.Sprintf("Failed to exchange token: %s", err.Error())})
|
||||
log.Printf("CompleteOIDC: failed to exchange token: %s", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: "Sorry, we are currently unable to respond to your request. Please retry later."})
|
||||
return
|
||||
}
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: "No id_token field in oauth2 token."})
|
||||
log.Printf("CompleteOIDC: no id_token field in oauth2 token")
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: "Sorry, we are currently unable to respond to your request. Please retry later."})
|
||||
return
|
||||
}
|
||||
|
||||
idToken, err := p.oidcVerifier.Verify(c, rawIDToken)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: fmt.Sprintf("Failed to verify ID Token: %s", err.Error())})
|
||||
log.Printf("CompleteOIDC: failed to verify ID Token: %s", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: "Sorry, we are currently unable to respond to your request. Please retry later."})
|
||||
return
|
||||
}
|
||||
|
||||
var claims map[string]interface{}
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: fmt.Sprintf("Unable to retrieve user profile: %s", err.Error())})
|
||||
if idToken.Nonce != expectedNonce {
|
||||
log.Printf("CompleteOIDC: nonce mismatch: got %q, expected %q", idToken.Nonce, expectedNonce)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, happydns.ErrorResponse{Message: "Invalid nonce in ID token"})
|
||||
return
|
||||
}
|
||||
|
||||
var claims map[string]any
|
||||
if err = idToken.Claims(&claims); err != nil {
|
||||
log.Printf("CompleteOIDC: unable to retrieve user profile: %s", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: "Sorry, we are currently unable to respond to your request. Please retry later."})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -171,17 +208,22 @@ func (p *OIDCProvider) CompleteOIDC(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
hash := sha1.Sum([]byte(profile.Email))
|
||||
hash := sha256.Sum256([]byte(profile.Email))
|
||||
profile.Id = hash[:]
|
||||
}
|
||||
|
||||
_, err = p.authService.CompleteAuthentication(&profile)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: fmt.Sprintf("Unable to complete authentication: %s", err.Error())})
|
||||
log.Printf("CompleteOIDC: unable to complete authentication: %s", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: "Sorry, we are currently unable to respond to your request. Please retry later."})
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SessionLoginOK(c, &profile)
|
||||
|
||||
c.Redirect(http.StatusFound, p.config.GetBaseURL()+"/")
|
||||
redirectTo := p.config.GetBaseURL() + "/"
|
||||
if nextPath != "" {
|
||||
redirectTo = p.config.GetBaseURL() + nextPath
|
||||
}
|
||||
c.Redirect(http.StatusFound, redirectTo)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ func (dc *DomainController) RetrieveZone(c *gin.Context) {
|
|||
}
|
||||
domain := c.MustGet("domain").(*happydns.Domain)
|
||||
|
||||
zone, err := dc.remoteZoneImporter.Import(user, domain)
|
||||
zone, err := dc.remoteZoneImporter.Import(c.Request.Context(), user, domain)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -48,13 +48,13 @@ type DNSMsg struct {
|
|||
Question []DNSQuestion
|
||||
|
||||
// Answer is the list of Answer records in the DNS response.
|
||||
Answer []interface{} `swaggertype:"object"`
|
||||
Answer []any `swaggertype:"object"`
|
||||
|
||||
// Ns is the list of Authoritative records in the DNS response.
|
||||
Ns []interface{} `swaggertype:"object"`
|
||||
Ns []any `swaggertype:"object"`
|
||||
|
||||
// Extra is the list of extra records in the DNS response.
|
||||
Extra []interface{} `swaggertype:"object"`
|
||||
Extra []any `swaggertype:"object"`
|
||||
}
|
||||
|
||||
type DNSQuestion struct {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ func (sc *ServiceController) AddZoneService(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, zone)
|
||||
}
|
||||
|
||||
// GetServiceService retrieves the designated Service.
|
||||
// GetZoneService retrieves the designated Service.
|
||||
//
|
||||
// @Summary Get the Service.
|
||||
// @Schemes
|
||||
|
|
|
|||
|
|
@ -22,22 +22,28 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/api/middleware"
|
||||
serviceUC "git.happydns.org/happyDomain/internal/usecase/service"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
type ServiceSpecsController struct {
|
||||
sSpecsServices happydns.ServiceSpecsUsecase
|
||||
sSpecsServices happydns.ServiceSpecsUsecase
|
||||
listRecordsService *serviceUC.ListRecordsUsecase
|
||||
}
|
||||
|
||||
func NewServiceSpecsController(sSpecsServices happydns.ServiceSpecsUsecase) *ServiceSpecsController {
|
||||
return &ServiceSpecsController{
|
||||
sSpecsServices: sSpecsServices,
|
||||
sSpecsServices: sSpecsServices,
|
||||
listRecordsService: serviceUC.NewListRecordsUsecase(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +85,7 @@ func (ssc *ServiceSpecsController) GetServiceSpecIcon(c *gin.Context) {
|
|||
c.Data(http.StatusOK, "image/png", cnt)
|
||||
}
|
||||
|
||||
// getServiceSpec returns a description of the expected fields.
|
||||
// GetServiceSpec returns a description of the expected fields.
|
||||
//
|
||||
// @Summary Get the service expected fields.
|
||||
// @Schemes
|
||||
|
|
@ -112,7 +118,7 @@ func (ssc *ServiceSpecsController) GetServiceSpec(c *gin.Context) {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param serviceType path string true "The service's type"
|
||||
// @Success 200 {object} interface{}
|
||||
// @Success 200 {object} any
|
||||
// @Failure 404 {object} happydns.ErrorResponse "Service type does not exist"
|
||||
// @Failure 500 {object} happydns.ErrorResponse "Internal error"
|
||||
// @Router /service_specs/{serviceType}/init [post]
|
||||
|
|
@ -127,3 +133,54 @@ func (ssc *ServiceSpecsController) InitializeServiceSpec(c *gin.Context) {
|
|||
|
||||
c.JSON(http.StatusOK, initialized)
|
||||
}
|
||||
|
||||
// GenerateRecords returns the DNS records that the service would generate.
|
||||
//
|
||||
// @Summary Generate DNS records for a service.
|
||||
// @Schemes
|
||||
// @Description Return the DNS records that the given service configuration would generate.
|
||||
// @Tags service_specs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param serviceType path string true "The service's type"
|
||||
// @Param domain query string true "The domain to use to generate the records"
|
||||
// @Param ttl query int false "The TTL used by the generated records"
|
||||
// @Success 200 {array} happydns.Record
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid request body"
|
||||
// @Failure 404 {object} happydns.ErrorResponse "Service type does not exist"
|
||||
// @Failure 500 {object} happydns.ErrorResponse "Internal error"
|
||||
// @Router /service_specs/{serviceType}/records [post]
|
||||
func (ssc *ServiceSpecsController) GenerateRecords(c *gin.Context) {
|
||||
svctype := c.MustGet("servicetype").(reflect.Type)
|
||||
domain := c.Query("domain")
|
||||
ttl, _ := strconv.Atoi(c.Query("ttl"))
|
||||
|
||||
if ttl == 0 {
|
||||
ttl = 3600
|
||||
}
|
||||
|
||||
svc, err := ssc.sSpecsServices.InitializeService(svctype)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.ShouldBindJSON(&svc)
|
||||
if err != nil {
|
||||
log.Printf("%s sends invalid domain JSON: %s", c.ClientIP(), err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Something is wrong in received data: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
records, err := ssc.listRecordsService.List(&happydns.Service{
|
||||
ServiceMeta: happydns.ServiceMeta{
|
||||
Domain: domain,
|
||||
},
|
||||
Service: svc.(happydns.ServiceBody),
|
||||
}, domain, uint32(ttl))
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, records)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ func (sc *SessionController) GetSession(c *gin.Context) {
|
|||
// @Accept json
|
||||
// @Prodsce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Ssccess 204 {null} null
|
||||
// @Ssccess 204
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /session [delete]
|
||||
func (sc *SessionController) ClearSession(c *gin.Context) {
|
||||
|
|
@ -230,16 +230,16 @@ func (sc *SessionController) UpdateSession(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
s, err := sc.sessionService.GetUserSession(myuser, c.Param("sid"))
|
||||
err = sc.sessionService.UpdateUserSession(myuser, c.Param("sid"), func(newsession *happydns.Session) {
|
||||
newsession.Description = us.Description
|
||||
newsession.ExpiresOn = us.ExpiresOn
|
||||
})
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = sc.sessionService.UpdateUserSession(myuser, c.Param("sid"), func(newsession *happydns.Session) {
|
||||
newsession.Description = us.Description
|
||||
newsession.ExpiresOn = us.ExpiresOn
|
||||
})
|
||||
s, err := sc.sessionService.GetUserSession(myuser, c.Param("sid"))
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
@ -256,9 +256,8 @@ func (sc *SessionController) UpdateSession(c *gin.Context) {
|
|||
// @Tags users
|
||||
// @Accept json
|
||||
// @Param sessionId path string true "Session identifier"
|
||||
// @Prodsce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Ssccess 200 {object} happydns.Session
|
||||
// @Success 204 {null} null
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /sessions/{sessionId} [delete]
|
||||
func (sc *SessionController) DeleteSession(c *gin.Context) {
|
||||
|
|
@ -274,5 +273,5 @@ func (sc *SessionController) DeleteSession(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, nil)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ func NewRegistrationController(auService happydns.AuthUserUsecase, captchaVerifi
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body happydns.UserRegistration true "Account information"
|
||||
// @Success 200 {object} happydns.User "The created user"
|
||||
// @Success 204
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /users [post]
|
||||
|
|
@ -66,7 +66,7 @@ func (rc *RegistrationController) RegisterNewUser(c *gin.Context) {
|
|||
}
|
||||
|
||||
if rc.captcha.Provider() != "" {
|
||||
if err := rc.captcha.Verify(uu.CaptchaToken, c.ClientIP()); err != nil {
|
||||
if err = rc.captcha.Verify(uu.CaptchaToken, c.ClientIP()); err != nil {
|
||||
log.Printf("%s: captcha verification failed during registration: %s", c.ClientIP(), err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, happydns.ErrorResponse{Message: "Captcha verification failed."})
|
||||
return
|
||||
|
|
@ -85,7 +85,10 @@ func (rc *RegistrationController) RegisterNewUser(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
log.Printf("%s: registers new user: %s", c.ClientIP(), user.Email)
|
||||
if user != nil {
|
||||
log.Printf("%s: registers new user: %s", c.ClientIP(), user.Email)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
// Always return the same response to prevent user enumeration.
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ func (zc *ZoneController) DiffZonesHandler(c *gin.Context) {
|
|||
var corrections []*happydns.Correction
|
||||
if c.Param("oldzoneid") == "@" {
|
||||
var err error
|
||||
corrections, nbDiffs, err = zc.zoneCorrectionService.List(user, domain, newzone)
|
||||
corrections, nbDiffs, err = zc.zoneCorrectionService.List(c.Request.Context(), user, domain, newzone)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
@ -214,7 +214,7 @@ func (zc *ZoneController) ApplyZoneCorrections(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
newZone, err := zc.zoneCorrectionService.Apply(user, domain, zone, &form)
|
||||
newZone, err := zc.zoneCorrectionService.Apply(c.Request.Context(), user, domain, zone, &form)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
@ -223,6 +223,46 @@ func (zc *ZoneController) ApplyZoneCorrections(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, newZone.ZoneMeta)
|
||||
}
|
||||
|
||||
// PrepareZoneCorrections computes the executable corrections without applying them.
|
||||
//
|
||||
// @Summary Preview the corrections the provider will execute.
|
||||
// @Schemes
|
||||
// @Description Compute the executable corrections for the selected changes without applying them.
|
||||
// @Tags zones
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Param domainId path string true "Domain identifier"
|
||||
// @Param zoneId path string true "Zone identifier"
|
||||
// @Param body body happydns.PrepareZoneForm true "Selected corrections to prepare"
|
||||
// @Success 200 {object} happydns.PrepareZoneResponse "The executable corrections"
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Failure 404 {object} happydns.ErrorResponse "Domain or Zone not found"
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domainId}/zone/{zoneId}/prepare_changes [post]
|
||||
func (zc *ZoneController) PrepareZoneCorrections(c *gin.Context) {
|
||||
user := c.MustGet("LoggedUser").(*happydns.User)
|
||||
domain := c.MustGet("domain").(*happydns.Domain)
|
||||
zone := c.MustGet("zone").(*happydns.Zone)
|
||||
|
||||
var form happydns.PrepareZoneForm
|
||||
err := c.ShouldBindJSON(&form)
|
||||
if err != nil {
|
||||
log.Printf("%s sends invalid PrepareZoneForm JSON: %s", c.ClientIP(), err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Something is wrong in received data: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
response, err := zc.zoneCorrectionService.Prepare(c.Request.Context(), user, domain, zone, &form)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// ExportZone creates a flatten export of the zone.
|
||||
//
|
||||
// @Summary Get flatten zone file.
|
||||
|
|
@ -279,8 +319,9 @@ func (zc *ZoneController) AddRecords(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var rr happydns.Record
|
||||
for _, record := range records {
|
||||
rr, err := helpers.ParseRecord(record, domain.DomainName)
|
||||
rr, err = helpers.ParseRecord(record, domain.DomainName)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
@ -340,8 +381,9 @@ func (zc *ZoneController) DeleteRecords(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var rr happydns.Record
|
||||
for _, record := range records {
|
||||
rr, err := helpers.ParseRecord(record, domain.DomainName)
|
||||
rr, err = helpers.ParseRecord(record, domain.DomainName)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
ginsessions "github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
gorillasessions "github.com/gorilla/sessions"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
|
@ -43,15 +44,49 @@ func AuthRequired() gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// gorillasessionExposer is satisfied by the concrete gin-contrib/sessions
|
||||
// type, which wraps a *gorilla/sessions.Session and exposes it via Session().
|
||||
// Using a duck-typed local interface avoids importing gin-contrib internals.
|
||||
type gorillasessionExposer interface {
|
||||
Session() *gorillasessions.Session
|
||||
}
|
||||
|
||||
func SessionLoginOK(c *gin.Context, user happydns.UserInfo) error {
|
||||
session := sessions.Default(c)
|
||||
session := ginsessions.Default(c)
|
||||
|
||||
// Phase 1: invalidate the pre-login session to prevent session fixation.
|
||||
// Preserve the original session options (Secure flag, Path, MaxAge) so
|
||||
// we can restore them on the new session.
|
||||
// Setting MaxAge=-1 causes the store to delete the server-side record and
|
||||
// send an expired cookie on Save().
|
||||
var origOptions *gorillasessions.Options
|
||||
if gs, ok := session.(gorillasessionExposer); ok {
|
||||
if gs.Session().Options != nil {
|
||||
opts := *gs.Session().Options // copy by value
|
||||
origOptions = &opts
|
||||
}
|
||||
}
|
||||
|
||||
session.Clear()
|
||||
session.Options(ginsessions.Options{MaxAge: -1})
|
||||
session.Save()
|
||||
|
||||
// Phase 2: create a genuinely new session with a fresh ID.
|
||||
// Reset the gorilla session's ID so the store generates a new one,
|
||||
// then restore the original cookie options.
|
||||
if gs, ok := session.(gorillasessionExposer); ok {
|
||||
gs.Session().ID = ""
|
||||
if origOptions != nil {
|
||||
origOptions.MaxAge = 86400 * 30 // restore positive MaxAge
|
||||
gs.Session().Options = origOptions
|
||||
}
|
||||
}
|
||||
|
||||
session.Set("iduser", user.GetUserId())
|
||||
err := session.Save()
|
||||
if err != nil {
|
||||
return happydns.InternalError{
|
||||
Err: fmt.Errorf("failed to save save user session: %s", err),
|
||||
Err: fmt.Errorf("failed to save user session: %s", err),
|
||||
UserMessage: "Invalid username or password.",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func JwtAuthMiddleware(authService happydns.AuthenticationUsecase, signingMethod
|
|||
// Validate the token and retrieve claims
|
||||
claims := &UserClaims{}
|
||||
_, err := jwt.ParseWithClaims(token, claims,
|
||||
func(token *jwt.Token) (interface{}, error) {
|
||||
func(token *jwt.Token) (any, error) {
|
||||
return secretKey, nil
|
||||
}, jwt.WithValidMethods([]string{signingMethod}))
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,14 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func DeclareAuthenticationRoutes(cfg *happydns.Options, baserouter, apirouter *gin.RouterGroup, authUC happydns.AuthenticationUsecase, captchaVerifier happydns.CaptchaVerifier, failureTracker happydns.FailureTracker) *controller.LoginController {
|
||||
func DeclareAuthenticationRoutes(
|
||||
cfg *happydns.Options,
|
||||
baserouter,
|
||||
apirouter *gin.RouterGroup,
|
||||
authUC happydns.AuthenticationUsecase,
|
||||
captchaVerifier happydns.CaptchaVerifier,
|
||||
failureTracker happydns.FailureTracker,
|
||||
) *controller.LoginController {
|
||||
lc := controller.NewLoginController(authUC, captchaVerifier, failureTracker)
|
||||
|
||||
apirouter.POST("/auth", lc.Login)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,17 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func DeclareDomainRoutes(router *gin.RouterGroup, domainUC happydns.DomainUsecase, domainLogUC happydns.DomainLogUsecase, remoteZoneImporter happydns.RemoteZoneImporterUsecase, zoneImporter happydns.ZoneImporterUsecase, zoneUC happydns.ZoneUsecase, zoneCorrApplier happydns.ZoneCorrectionApplierUsecase, zoneServiceUC happydns.ZoneServiceUsecase, serviceUC happydns.ServiceUsecase) {
|
||||
func DeclareDomainRoutes(
|
||||
router *gin.RouterGroup,
|
||||
domainUC happydns.DomainUsecase,
|
||||
domainLogUC happydns.DomainLogUsecase,
|
||||
remoteZoneImporter happydns.RemoteZoneImporterUsecase,
|
||||
zoneImporter happydns.ZoneImporterUsecase,
|
||||
zoneUC happydns.ZoneUsecase,
|
||||
zoneCorrApplier happydns.ZoneCorrectionApplierUsecase,
|
||||
zoneServiceUC happydns.ZoneServiceUsecase,
|
||||
serviceUC happydns.ServiceUsecase,
|
||||
) {
|
||||
dc := controller.NewDomainController(
|
||||
domainUC,
|
||||
remoteZoneImporter,
|
||||
|
|
@ -51,5 +61,12 @@ func DeclareDomainRoutes(router *gin.RouterGroup, domainUC happydns.DomainUsecas
|
|||
apiDomainsRoutes.POST("/zone", dc.ImportZone)
|
||||
apiDomainsRoutes.POST("/retrieve_zone", dc.RetrieveZone)
|
||||
|
||||
DeclareZoneRoutes(apiDomainsRoutes, zoneUC, domainUC, zoneCorrApplier, zoneServiceUC, serviceUC)
|
||||
DeclareZoneRoutes(
|
||||
apiDomainsRoutes,
|
||||
zoneUC,
|
||||
domainUC,
|
||||
zoneCorrApplier,
|
||||
zoneServiceUC,
|
||||
serviceUC,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,14 +72,21 @@ type Dependencies struct {
|
|||
// @name Authorization
|
||||
// @description Description for what is this security definition being used
|
||||
|
||||
func DeclareRoutes(cfg *happydns.Options, router *gin.Engine, dep Dependencies) {
|
||||
func DeclareRoutes(cfg *happydns.Options, router *gin.RouterGroup, dep Dependencies) {
|
||||
baseRoutes := router.Group("")
|
||||
|
||||
declareRouteSwagger(cfg, baseRoutes)
|
||||
|
||||
apiRoutes := router.Group("/api")
|
||||
|
||||
lc := DeclareAuthenticationRoutes(cfg, baseRoutes, apiRoutes, dep.Authentication, dep.CaptchaVerifier, dep.FailureTracker)
|
||||
lc := DeclareAuthenticationRoutes(
|
||||
cfg,
|
||||
baseRoutes,
|
||||
apiRoutes,
|
||||
dep.Authentication,
|
||||
dep.CaptchaVerifier,
|
||||
dep.FailureTracker,
|
||||
)
|
||||
auc := DeclareAuthUserRoutes(apiRoutes, dep.AuthUser, lc)
|
||||
DeclareProviderSpecsRoutes(apiRoutes, dep.ProviderSpecs)
|
||||
DeclareRegistrationRoutes(apiRoutes, dep.AuthUser, dep.CaptchaVerifier)
|
||||
|
|
@ -99,7 +106,17 @@ func DeclareRoutes(cfg *happydns.Options, router *gin.Engine, dep Dependencies)
|
|||
apiAuthRoutes.Use(middleware.AuthRequired())
|
||||
|
||||
DeclareAuthenticationCheckRoutes(apiAuthRoutes, lc)
|
||||
DeclareDomainRoutes(apiAuthRoutes, dep.Domain, dep.DomainLog, dep.RemoteZoneImporter, dep.ZoneImporter, dep.Zone, dep.ZoneCorrectionApplier, dep.ZoneService, dep.Service)
|
||||
DeclareDomainRoutes(
|
||||
apiAuthRoutes,
|
||||
dep.Domain,
|
||||
dep.DomainLog,
|
||||
dep.RemoteZoneImporter,
|
||||
dep.ZoneImporter,
|
||||
dep.Zone,
|
||||
dep.ZoneCorrectionApplier,
|
||||
dep.ZoneService,
|
||||
dep.Service,
|
||||
)
|
||||
DeclareProviderRoutes(apiAuthRoutes, dep.Provider)
|
||||
DeclareProviderSettingsRoutes(apiAuthRoutes, dep.ProviderSettings)
|
||||
DeclareRecordRoutes(apiAuthRoutes)
|
||||
|
|
|
|||
|
|
@ -29,15 +29,22 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func DeclareZoneServiceRoutes(apiZonesRoutes, apiZonesSubdomainRoutes *gin.RouterGroup, zc *controller.ZoneController, zoneServiceUC happydns.ZoneServiceUsecase, serviceUC happydns.ServiceUsecase, zoneUC happydns.ZoneUsecase) {
|
||||
func DeclareZoneServiceRoutes(
|
||||
apiZonesRoutes,
|
||||
apiZonesSubdomainRoutes *gin.RouterGroup,
|
||||
zc *controller.ZoneController,
|
||||
zoneServiceUC happydns.ZoneServiceUsecase,
|
||||
serviceUC happydns.ServiceUsecase,
|
||||
zoneUC happydns.ZoneUsecase,
|
||||
) {
|
||||
sc := controller.NewServiceController(zoneServiceUC, serviceUC, zoneUC)
|
||||
|
||||
apiZonesRoutes.PATCH("", sc.UpdateZoneService)
|
||||
|
||||
apiZonesSubdomainRoutes.POST("/services", sc.AddZoneService)
|
||||
|
||||
apiZonesSubdomainServiceIdRoutes := apiZonesSubdomainRoutes.Group("/services/:serviceid")
|
||||
apiZonesSubdomainServiceIdRoutes.Use(middleware.ServiceIdHandler(serviceUC))
|
||||
apiZonesSubdomainServiceIdRoutes.GET("", sc.GetZoneService)
|
||||
apiZonesSubdomainServiceIdRoutes.DELETE("", sc.DeleteZoneService)
|
||||
apiZonesSubdomainServiceIDRoutes := apiZonesSubdomainRoutes.Group("/services/:serviceid")
|
||||
apiZonesSubdomainServiceIDRoutes.Use(middleware.ServiceIdHandler(serviceUC))
|
||||
apiZonesSubdomainServiceIDRoutes.GET("", sc.GetZoneService)
|
||||
apiZonesSubdomainServiceIDRoutes.DELETE("", sc.DeleteZoneService)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,4 +41,5 @@ func DeclareServiceSpecsRoutes(router *gin.RouterGroup, serviceSpecsUC happydns.
|
|||
|
||||
apiServiceSpecsRoutes.GET("", ssc.GetServiceSpec)
|
||||
apiServiceSpecsRoutes.POST("/init", ssc.InitializeServiceSpec)
|
||||
apiServiceSpecsRoutes.POST("/records", ssc.GenerateRecords)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,14 @@ import (
|
|||
happydns "git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func DeclareZoneRoutes(router *gin.RouterGroup, zoneUC happydns.ZoneUsecase, domainUC happydns.DomainUsecase, zoneCorrApplier happydns.ZoneCorrectionApplierUsecase, zoneServiceUC happydns.ZoneServiceUsecase, serviceUC happydns.ServiceUsecase) {
|
||||
func DeclareZoneRoutes(
|
||||
router *gin.RouterGroup,
|
||||
zoneUC happydns.ZoneUsecase,
|
||||
domainUC happydns.DomainUsecase,
|
||||
zoneCorrApplier happydns.ZoneCorrectionApplierUsecase,
|
||||
zoneServiceUC happydns.ZoneServiceUsecase,
|
||||
serviceUC happydns.ServiceUsecase,
|
||||
) {
|
||||
zc := controller.NewZoneController(
|
||||
zoneUC,
|
||||
domainUC,
|
||||
|
|
@ -44,13 +51,21 @@ func DeclareZoneRoutes(router *gin.RouterGroup, zoneUC happydns.ZoneUsecase, dom
|
|||
apiZonesRoutes.POST("/diff/:oldzoneid", zc.DiffZonesHandler, zc.DiffZones)
|
||||
apiZonesRoutes.POST("/diff/:oldzoneid/summary", zc.DiffZonesHandler, zc.DiffZonesSummary)
|
||||
apiZonesRoutes.POST("/view", zc.ExportZone)
|
||||
apiZonesRoutes.POST("/prepare_changes", zc.PrepareZoneCorrections)
|
||||
apiZonesRoutes.POST("/apply_changes", zc.ApplyZoneCorrections)
|
||||
|
||||
apiZonesSubdomainRoutes := apiZonesRoutes.Group("/:subdomain")
|
||||
apiZonesSubdomainRoutes.Use(middleware.SubdomainHandler)
|
||||
apiZonesSubdomainRoutes.GET("", zc.GetZoneSubdomain)
|
||||
|
||||
DeclareZoneServiceRoutes(apiZonesRoutes, apiZonesSubdomainRoutes, zc, zoneServiceUC, serviceUC, zoneUC)
|
||||
DeclareZoneServiceRoutes(
|
||||
apiZonesRoutes,
|
||||
apiZonesSubdomainRoutes,
|
||||
zc,
|
||||
zoneServiceUC,
|
||||
serviceUC,
|
||||
zoneUC,
|
||||
)
|
||||
|
||||
apiZonesRoutes.POST("/records", zc.AddRecords)
|
||||
apiZonesRoutes.POST("/records/delete", zc.DeleteRecords)
|
||||
|
|
|
|||
|
|
@ -54,20 +54,25 @@ func NewAdmin(app *App) *Admin {
|
|||
router.Use(gin.Logger(), gin.Recovery())
|
||||
|
||||
// Prepare usecases (admin uses unrestricted provider access)
|
||||
app.usecases.providerAdmin = providerUC.NewService(app.store)
|
||||
app.usecases.providerAdmin = providerUC.NewService(app.store, nil)
|
||||
|
||||
admin.DeclareRoutes(app.cfg, router, app.store, admin.Dependencies{
|
||||
AuthUser: app.usecases.authUser,
|
||||
Domain: app.usecases.domain,
|
||||
Provider: app.usecases.providerAdmin,
|
||||
RemoteZoneImporter: app.usecases.orchestrator.RemoteZoneImporter,
|
||||
Service: app.usecases.service,
|
||||
User: app.usecases.user,
|
||||
Zone: app.usecases.zone,
|
||||
ZoneCorrectionApplier: app.usecases.orchestrator.ZoneCorrectionApplier,
|
||||
ZoneImporter: app.usecases.orchestrator.ZoneImporter,
|
||||
ZoneService: app.usecases.zoneService,
|
||||
})
|
||||
admin.DeclareRoutes(
|
||||
app.cfg,
|
||||
router,
|
||||
app.store,
|
||||
admin.Dependencies{
|
||||
AuthUser: app.usecases.authUser,
|
||||
Domain: app.usecases.domain,
|
||||
Provider: app.usecases.providerAdmin,
|
||||
RemoteZoneImporter: app.usecases.orchestrator.RemoteZoneImporter,
|
||||
Service: app.usecases.service,
|
||||
User: app.usecases.user,
|
||||
Zone: app.usecases.zone,
|
||||
ZoneCorrectionApplier: app.usecases.orchestrator.ZoneCorrectionApplier,
|
||||
ZoneImporter: app.usecases.orchestrator.ZoneImporter,
|
||||
ZoneService: app.usecases.zoneService,
|
||||
},
|
||||
)
|
||||
web.DeclareRoutes(app.cfg, router)
|
||||
|
||||
return &Admin{
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ type App struct {
|
|||
cfg *happydns.Options
|
||||
failureTracker *captcha.FailureTracker
|
||||
insights *insightsCollector
|
||||
mailer *mailer.Mailer
|
||||
mailer happydns.Mailer
|
||||
newsletter happydns.NewsletterSubscriptor
|
||||
router *gin.Engine
|
||||
srv *http.Server
|
||||
|
|
@ -84,7 +84,6 @@ type App struct {
|
|||
usecases Usecases
|
||||
}
|
||||
|
||||
|
||||
func NewApp(cfg *happydns.Options) *App {
|
||||
app := &App{
|
||||
cfg: cfg,
|
||||
|
|
@ -94,6 +93,9 @@ func NewApp(cfg *happydns.Options) *App {
|
|||
app.initStorageEngine()
|
||||
app.initNewsletter()
|
||||
app.initInsights()
|
||||
if err := app.initPlugins(); err != nil {
|
||||
log.Fatalf("Plugin initialization error: %s", err)
|
||||
}
|
||||
app.initUsecases()
|
||||
app.initCaptcha()
|
||||
app.setupRouter()
|
||||
|
|
@ -109,6 +111,9 @@ func NewAppWithStorage(cfg *happydns.Options, store storage.Storage) *App {
|
|||
|
||||
app.initMailer()
|
||||
app.initNewsletter()
|
||||
if err := app.initPlugins(); err != nil {
|
||||
log.Fatalf("Plugin initialization error: %s", err)
|
||||
}
|
||||
app.initUsecases()
|
||||
app.initCaptcha()
|
||||
app.setupRouter()
|
||||
|
|
@ -129,19 +134,22 @@ func (app *App) initCaptcha() {
|
|||
|
||||
func (app *App) initMailer() {
|
||||
if app.cfg.MailSMTPHost != "" {
|
||||
app.mailer = &mailer.Mailer{
|
||||
m := &mailer.Mailer{
|
||||
MailFrom: &app.cfg.MailFrom,
|
||||
SendMethod: mailer.NewSMTPMailer(app.cfg.MailSMTPHost, app.cfg.MailSMTPPort, app.cfg.MailSMTPUsername, app.cfg.MailSMTPPassword),
|
||||
}
|
||||
|
||||
if app.cfg.MailSMTPTLSSNoVerify {
|
||||
app.mailer.SendMethod.(*mailer.SMTPMailer).WithTLSNoVerify()
|
||||
m.SendMethod.(*mailer.SMTPMailer).WithTLSNoVerify()
|
||||
}
|
||||
app.mailer = m
|
||||
} else if !app.cfg.NoMail {
|
||||
app.mailer = &mailer.Mailer{
|
||||
MailFrom: &app.cfg.MailFrom,
|
||||
SendMethod: &mailer.SystemSendmail{},
|
||||
}
|
||||
} else {
|
||||
app.mailer = &mailer.LogMailer{}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +175,7 @@ func (app *App) initNewsletter() {
|
|||
if app.cfg.ListmonkURL.String() != "" {
|
||||
app.newsletter = &newsletter.ListmonkNewsletterSubscription{
|
||||
ListmonkURL: &app.cfg.ListmonkURL,
|
||||
ListmonkId: app.cfg.ListmonkId,
|
||||
ListmonkID: app.cfg.ListmonkID,
|
||||
}
|
||||
} else {
|
||||
app.newsletter = &newsletter.DummyNewsletterSubscription{}
|
||||
|
|
@ -186,27 +194,48 @@ func (app *App) initInsights() {
|
|||
|
||||
func (app *App) initUsecases() {
|
||||
sessionService := sessionUC.NewService(app.store)
|
||||
authUserService := authuserUC.NewAuthUserUsecases(app.cfg, app.mailer, app.store, sessionService)
|
||||
authUserService := authuserUC.NewAuthUserUsecases(
|
||||
app.cfg,
|
||||
app.mailer,
|
||||
app.store,
|
||||
sessionService,
|
||||
)
|
||||
domainLogService := domainlogUC.NewService(app.store)
|
||||
providerService := providerUC.NewRestrictedService(app.cfg, app.store)
|
||||
providerAdminService := providerUC.NewService(app.store)
|
||||
providerAdminService := providerUC.NewService(app.store, nil)
|
||||
serviceService := serviceUC.NewServiceUsecases()
|
||||
zoneService := zoneUC.NewZoneUsecases(app.store, serviceService)
|
||||
|
||||
app.usecases.providerSpecs = usecase.NewProviderSpecsUsecase()
|
||||
app.usecases.provider = providerService
|
||||
app.usecases.providerAdmin = providerAdminService
|
||||
app.usecases.providerSettings = usecase.NewProviderSettingsUsecase(app.cfg, app.usecases.provider, app.store)
|
||||
app.usecases.providerSettings = usecase.NewProviderSettingsUsecase(app.cfg, app.usecases.provider)
|
||||
app.usecases.service = serviceService
|
||||
app.usecases.serviceSpecs = usecase.NewServiceSpecsUsecase()
|
||||
app.usecases.zone = zoneService
|
||||
app.usecases.domainLog = domainLogService
|
||||
|
||||
domainService := domainUC.NewService(app.store, providerAdminService, zoneService.GetZoneUC, providerAdminService, domainLogService)
|
||||
domainService := domainUC.NewService(
|
||||
app.store,
|
||||
providerAdminService,
|
||||
zoneService.GetZoneUC,
|
||||
providerAdminService,
|
||||
domainLogService,
|
||||
)
|
||||
app.usecases.domain = domainService
|
||||
app.usecases.zoneService = zoneServiceUC.NewZoneServiceUsecases(domainService, zoneService.CreateZoneUC, serviceService.ValidateServiceUC, app.store)
|
||||
app.usecases.zoneService = zoneServiceUC.NewZoneServiceUsecases(
|
||||
domainService,
|
||||
zoneService.CreateZoneUC,
|
||||
serviceService.ValidateServiceUC,
|
||||
app.store,
|
||||
)
|
||||
|
||||
app.usecases.user = userUC.NewUserUsecases(app.store, app.newsletter, authUserService, sessionService)
|
||||
app.usecases.user = userUC.NewUserUsecases(
|
||||
app.store,
|
||||
app.newsletter,
|
||||
authUserService,
|
||||
sessionService,
|
||||
)
|
||||
app.usecases.authentication = usecase.NewAuthenticationUsecase(app.cfg, app.store, app.usecases.user)
|
||||
app.usecases.authUser = authUserService
|
||||
app.usecases.resolver = usecase.NewResolverUsecase(app.cfg)
|
||||
|
|
@ -219,6 +248,7 @@ func (app *App) initUsecases() {
|
|||
zoneService.ListRecordsUC,
|
||||
providerAdminService,
|
||||
zoneService.CreateZoneUC,
|
||||
zoneService.GetZoneUC,
|
||||
providerAdminService,
|
||||
zoneService.UpdateZoneUC,
|
||||
)
|
||||
|
|
@ -236,28 +266,41 @@ func (app *App) setupRouter() {
|
|||
session.NewSessionStore(app.cfg, app.store, []byte(app.cfg.JWTSecretKey)),
|
||||
))
|
||||
|
||||
api.DeclareRoutes(app.cfg, app.router, api.Dependencies{
|
||||
Authentication: app.usecases.authentication,
|
||||
AuthUser: app.usecases.authUser,
|
||||
CaptchaVerifier: app.captchaVerifier,
|
||||
Domain: app.usecases.domain,
|
||||
DomainLog: app.usecases.domainLog,
|
||||
FailureTracker: app.failureTracker,
|
||||
Provider: app.usecases.provider,
|
||||
ProviderSettings: app.usecases.providerSettings,
|
||||
ProviderSpecs: app.usecases.providerSpecs,
|
||||
RemoteZoneImporter: app.usecases.orchestrator.RemoteZoneImporter,
|
||||
Resolver: app.usecases.resolver,
|
||||
Service: app.usecases.service,
|
||||
ServiceSpecs: app.usecases.serviceSpecs,
|
||||
Session: app.usecases.session,
|
||||
User: app.usecases.user,
|
||||
Zone: app.usecases.zone,
|
||||
ZoneCorrectionApplier: app.usecases.orchestrator.ZoneCorrectionApplier,
|
||||
ZoneImporter: app.usecases.orchestrator.ZoneImporter,
|
||||
ZoneService: app.usecases.zoneService,
|
||||
})
|
||||
web.DeclareRoutes(app.cfg, app.router, app.captchaVerifier)
|
||||
if len(app.cfg.BasePath) > 0 {
|
||||
app.router.GET("/", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, app.cfg.BasePath)
|
||||
})
|
||||
}
|
||||
|
||||
baserouter := app.router.Group(app.cfg.BasePath)
|
||||
|
||||
api.DeclareRoutes(
|
||||
app.cfg,
|
||||
baserouter,
|
||||
api.Dependencies{
|
||||
Authentication: app.usecases.authentication,
|
||||
AuthUser: app.usecases.authUser,
|
||||
CaptchaVerifier: app.captchaVerifier,
|
||||
Domain: app.usecases.domain,
|
||||
DomainLog: app.usecases.domainLog,
|
||||
FailureTracker: app.failureTracker,
|
||||
Provider: app.usecases.provider,
|
||||
ProviderSettings: app.usecases.providerSettings,
|
||||
ProviderSpecs: app.usecases.providerSpecs,
|
||||
RemoteZoneImporter: app.usecases.orchestrator.RemoteZoneImporter,
|
||||
Resolver: app.usecases.resolver,
|
||||
Service: app.usecases.service,
|
||||
ServiceSpecs: app.usecases.serviceSpecs,
|
||||
Session: app.usecases.session,
|
||||
User: app.usecases.user,
|
||||
Zone: app.usecases.zone,
|
||||
ZoneCorrectionApplier: app.usecases.orchestrator.ZoneCorrectionApplier,
|
||||
ZoneImporter: app.usecases.orchestrator.ZoneImporter,
|
||||
ZoneService: app.usecases.zoneService,
|
||||
},
|
||||
)
|
||||
web.DeclareRoutes(app.cfg, baserouter, app.captchaVerifier)
|
||||
web.NoRoute(app.cfg, app.router)
|
||||
}
|
||||
|
||||
func (app *App) Start() {
|
||||
|
|
|
|||
184
internal/app/plugins.go
Normal file
184
internal/app/plugins.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
|
||||
"git.happydns.org/happyDomain/checks"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/providers"
|
||||
"git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
// pluginLoader attempts to find and register one specific kind of plugin
|
||||
// symbol from an already-opened .so file.
|
||||
//
|
||||
// It returns (true, nil) when the symbol was found and registration
|
||||
// succeeded, (true, err) when the symbol was found but something went wrong,
|
||||
// and (false, nil) when the symbol simply isn't present in that file (which
|
||||
// is not considered an error — a single .so may implement only a subset of
|
||||
// the known plugin types).
|
||||
type pluginLoader func(p *plugin.Plugin, fname string) (found bool, err error)
|
||||
|
||||
// pluginLoaders is the authoritative list of plugin types that happyDomain
|
||||
// knows about. To support a new plugin type, add a single entry here.
|
||||
var pluginLoaders = []pluginLoader{
|
||||
loadCheckPlugin,
|
||||
loadProviderPlugin,
|
||||
loadServicePlugin,
|
||||
}
|
||||
|
||||
// loadCheckPlugin handles the NewTestPlugin symbol.
|
||||
func loadCheckPlugin(p *plugin.Plugin, fname string) (bool, error) {
|
||||
sym, err := p.Lookup("NewCheckPlugin")
|
||||
if err != nil {
|
||||
// Symbol not present in this .so — not an error.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
factory, ok := sym.(func() (string, happydns.Checker, error))
|
||||
if !ok {
|
||||
return true, fmt.Errorf("symbol NewCheckPlugin has unexpected type %T", sym)
|
||||
}
|
||||
|
||||
pluginname, myplugin, err := factory()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
checks.RegisterChecker(pluginname, myplugin)
|
||||
log.Printf("Plugin %s loaded", pluginname)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// loadProviderPlugin handles the NewProviderPlugin symbol.
|
||||
func loadProviderPlugin(_ *PluginManager, p *plugin.Plugin, fname string) (bool, error) {
|
||||
sym, err := p.Lookup("NewProviderPlugin")
|
||||
if err != nil {
|
||||
// Symbol not present in this .so — not an error.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
factory, ok := sym.(func() (happydns.ProviderCreatorFunc, happydns.ProviderInfos, error))
|
||||
if !ok {
|
||||
return true, fmt.Errorf("symbol NewProviderPlugin has unexpected type %T", sym)
|
||||
}
|
||||
|
||||
creator, infos, err := factory()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
providers.RegisterProvider(creator, infos)
|
||||
log.Printf("Plugin provider %q registered from %s", infos.Name, fname)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// loadServicePlugin handles the NewServicePlugin symbol.
|
||||
func loadServicePlugin(_ *PluginManager, p *plugin.Plugin, fname string) (bool, error) {
|
||||
sym, err := p.Lookup("NewServicePlugin")
|
||||
if err != nil {
|
||||
// Symbol not present in this .so — not an error.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
factory, ok := sym.(func() (happydns.ServiceCreator, svcs.ServiceAnalyzer, happydns.ServiceInfos, uint32, []string, error))
|
||||
if !ok {
|
||||
return true, fmt.Errorf("symbol NewServicePlugin has unexpected type %T", sym)
|
||||
}
|
||||
|
||||
creator, analyzer, infos, weight, aliases, err := factory()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
svcs.RegisterService(creator, analyzer, infos, weight, aliases...)
|
||||
log.Printf("Plugin service %q registered from %s", infos.Name, fname)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// initPlugins scans each directory listed in cfg.PluginsDirectories, loads
|
||||
// every .so file found as a Go plugin, and registers it in the application's
|
||||
// PluginManager. All load errors are collected and returned as a joined error
|
||||
// so that a single bad plugin does not prevent the others from loading.
|
||||
func (a *App) initPlugins() error {
|
||||
for _, directory := range a.cfg.PluginsDirectories {
|
||||
files, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read plugins directory %q: %s", directory, err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only attempt to load shared-object files.
|
||||
if filepath.Ext(file.Name()) != ".so" {
|
||||
continue
|
||||
}
|
||||
|
||||
fname := filepath.Join(directory, file.Name())
|
||||
|
||||
err = loadPlugin(fname)
|
||||
if err != nil {
|
||||
log.Printf("Unable to load plugin %q: %s", fname, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadPlugin opens the .so file at fname and runs every registered
|
||||
// pluginLoader against it. A loader that does not find its symbol is silently
|
||||
// skipped. If no loader recognises any symbol in the file a warning is logged,
|
||||
// but no error is returned because the file might be a valid plugin for a
|
||||
// future version of happyDomain. The first loader error that is encountered
|
||||
// is returned immediately.
|
||||
func loadPlugin(fname string) error {
|
||||
p, err := plugin.Open(fname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
anyFound := false
|
||||
for _, loader := range pluginLoaders {
|
||||
found, err := loader(p, fname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if found {
|
||||
anyFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !anyFound {
|
||||
log.Printf("Warning: plugin %q exports no recognised symbols", fname)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ func declareFlags(o *happydns.Options) {
|
|||
flag.BoolVar(&o.OptOutInsights, "opt-out-insights", false, "Disable the anonymous usage statistics report. If you care about this project and don't participate in discussions, don't opt-out.")
|
||||
|
||||
flag.Var(&URL{&o.ListmonkURL}, "newsletter-server-url", "Base URL of the listmonk newsletter server")
|
||||
flag.IntVar(&o.ListmonkId, "newsletter-id", 1, "Listmonk identifier of the list receiving the new user")
|
||||
flag.IntVar(&o.ListmonkID, "newsletter-id", 1, "Listmonk identifier of the list receiving the new user")
|
||||
|
||||
flag.BoolVar(&o.NoMail, "no-mail", o.NoMail, "Disable all automatic mails, skip email verification at registration")
|
||||
flag.Var(&mailAddress{&o.MailFrom}, "mail-from", "Define the sender name and address for all e-mail sent")
|
||||
|
|
@ -60,6 +60,8 @@ func declareFlags(o *happydns.Options) {
|
|||
flag.StringVar(&o.CaptchaProvider, "captcha-provider", o.CaptchaProvider, "Captcha provider to use for bot protection (altcha, hcaptcha, recaptchav2, turnstile, or empty to disable)")
|
||||
flag.IntVar(&o.CaptchaLoginThreshold, "captcha-login-threshold", 3, "Number of failed login attempts before captcha is required (0 = always require when provider configured)")
|
||||
|
||||
flag.Var(&ArrayArgs{&o.PluginsDirectories}, "plugins-directory", "Path to a directory containing plugins (can be repeated multiple times)")
|
||||
|
||||
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func ConsolidateConfig() (opts *happydns.Options, err error) {
|
|||
"happydomain.conf",
|
||||
}
|
||||
|
||||
if home, err := os.UserConfigDir(); err == nil {
|
||||
if home, dirErr := os.UserConfigDir(); dirErr == nil {
|
||||
configLocations = append(configLocations, path.Join(home, "happydomain", "happydomain.conf"))
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +98,10 @@ func ConsolidateConfig() (opts *happydns.Options, err error) {
|
|||
} else {
|
||||
opts.BasePath = ""
|
||||
}
|
||||
if opts.DevProxy != "" && opts.BasePath != "" {
|
||||
err = fmt.Errorf("-base-path is not supported in -dev mode")
|
||||
return
|
||||
}
|
||||
|
||||
if opts.NoMail && opts.MailSMTPHost != "" {
|
||||
err = fmt.Errorf("-no-mail and -mail-smtp-* cannot be defined at the same time")
|
||||
|
|
@ -143,17 +147,17 @@ func ConsolidateConfig() (opts *happydns.Options, err error) {
|
|||
|
||||
// parseLine treats a config line and place the read value in the variable
|
||||
// declared to the corresponding flag.
|
||||
func parseLine(o *happydns.Options, line string) (err error) {
|
||||
func parseLine(_ *happydns.Options, line string) (err error) {
|
||||
fields := strings.SplitN(line, "=", 2)
|
||||
orig_key := strings.TrimSpace(fields[0])
|
||||
origKey := strings.TrimSpace(fields[0])
|
||||
value := strings.TrimSpace(fields[1])
|
||||
|
||||
if len(value) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
key := strings.TrimPrefix(strings.TrimPrefix(orig_key, "HAPPYDNS_"), "HAPPYDOMAIN_")
|
||||
key = strings.Replace(key, "_", "-", -1)
|
||||
key := strings.TrimPrefix(strings.TrimPrefix(origKey, "HAPPYDNS_"), "HAPPYDOMAIN_")
|
||||
key = strings.ReplaceAll(key, "_", "-")
|
||||
key = strings.ToLower(key)
|
||||
|
||||
err = flag.Set(key, value)
|
||||
|
|
|
|||
|
|
@ -25,8 +25,25 @@ import (
|
|||
"encoding/base64"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ArrayArgs struct {
|
||||
Slice *[]string
|
||||
}
|
||||
|
||||
func (i *ArrayArgs) String() string {
|
||||
if i == nil || i.Slice == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(*i.Slice, ",")
|
||||
}
|
||||
|
||||
func (i *ArrayArgs) Set(value string) error {
|
||||
*i.Slice = append(*i.Slice, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
type JWTSecretKey struct {
|
||||
Secret *[]byte
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,9 +44,8 @@ func GenField(field reflect.StructField) (f *happydns.Field) {
|
|||
}
|
||||
|
||||
tag := field.Tag.Get("happydomain")
|
||||
tuples := strings.Split(tag, ",")
|
||||
|
||||
for _, t := range tuples {
|
||||
for t := range strings.SplitSeq(tag, ",") {
|
||||
kv := strings.SplitN(t, "=", 2)
|
||||
if len(kv) > 1 {
|
||||
switch strings.ToLower(kv[0]) {
|
||||
|
|
@ -80,7 +79,7 @@ func GenField(field reflect.StructField) (f *happydns.Field) {
|
|||
}
|
||||
|
||||
// GenStructFields generates corresponding SourceFields of the given Source.
|
||||
func GenStructFields(data interface{}) (fields []*happydns.Field) {
|
||||
func GenStructFields(data any) (fields []*happydns.Field) {
|
||||
if data != nil {
|
||||
dataMeta := reflect.Indirect(reflect.ValueOf(data)).Type()
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func DoSettingState(fu happydns.FormUsecase, state *happydns.FormState, data interface{}, defaultForm func(interface{}) *happydns.CustomForm) (form *happydns.CustomForm, d map[string]interface{}, err error) {
|
||||
func DoSettingState(fu happydns.FormUsecase, state *happydns.FormState, data any, defaultForm func(any) *happydns.CustomForm) (form *happydns.CustomForm, d map[string]any, err error) {
|
||||
if csf, ok := data.(happydns.CustomSettingsForm); ok {
|
||||
return csf.DisplaySettingsForm(state.State, func() string {
|
||||
return state.Recall
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import (
|
|||
)
|
||||
|
||||
// GenDefaultSettingsForm generates a generic CustomForm presenting all the fields in one page.
|
||||
func GenDefaultSettingsForm(data interface{}) *happydns.CustomForm {
|
||||
func GenDefaultSettingsForm(data any) *happydns.CustomForm {
|
||||
return &happydns.CustomForm{
|
||||
Fields: GenStructFields(data),
|
||||
NextButtonText: "common.create-thing",
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func GeneratePassword() (password string, err error) {
|
|||
// This one is to avoid problem with openssl
|
||||
{"/", "^"},
|
||||
} {
|
||||
password = strings.Replace(password, i[0], i[1], -1)
|
||||
password = strings.ReplaceAll(password, i[0], i[1])
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ func TestGeneratePassword(t *testing.T) {
|
|||
t.Run("password does not contain replaced characters", func(t *testing.T) {
|
||||
forbiddenChars := []string{"v", "u", "l", "1", "o", "O", "0", "/"}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
password, err := GeneratePassword()
|
||||
if err != nil {
|
||||
t.Fatalf("GeneratePassword() returned error: %v", err)
|
||||
|
|
@ -59,7 +59,7 @@ func TestGeneratePassword(t *testing.T) {
|
|||
replacementChars := []string{"*", "(", "%", "?", "@", "!", ">", "^"}
|
||||
foundChars := make(map[string]bool)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
for range 1000 {
|
||||
password, err := GeneratePassword()
|
||||
if err != nil {
|
||||
t.Fatalf("GeneratePassword() returned error: %v", err)
|
||||
|
|
@ -81,7 +81,7 @@ func TestGeneratePassword(t *testing.T) {
|
|||
passwords := make(map[string]bool)
|
||||
iterations := 100
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
for range iterations {
|
||||
password, err := GeneratePassword()
|
||||
if err != nil {
|
||||
t.Fatalf("GeneratePassword() returned error: %v", err)
|
||||
|
|
@ -98,7 +98,7 @@ func TestGeneratePassword(t *testing.T) {
|
|||
t.Run("password uses valid characters", func(t *testing.T) {
|
||||
validChars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+*(%?@!>^="
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
password, err := GeneratePassword()
|
||||
if err != nil {
|
||||
t.Fatalf("GeneratePassword() returned error: %v", err)
|
||||
|
|
@ -116,7 +116,7 @@ func TestGeneratePassword(t *testing.T) {
|
|||
charCounts := make(map[rune]int)
|
||||
iterations := 1000
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
for range iterations {
|
||||
password, err := GeneratePassword()
|
||||
if err != nil {
|
||||
t.Fatalf("GeneratePassword() returned error: %v", err)
|
||||
|
|
@ -134,7 +134,7 @@ func TestGeneratePassword(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("password ends with valid character", func(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
password, err := GeneratePassword()
|
||||
if err != nil {
|
||||
t.Fatalf("GeneratePassword() returned error: %v", err)
|
||||
|
|
@ -162,7 +162,7 @@ func TestGeneratePasswordNonEmpty(t *testing.T) {
|
|||
func TestGeneratePasswordConsistentLength(t *testing.T) {
|
||||
lengths := make(map[int]int)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
for range 1000 {
|
||||
password, err := GeneratePassword()
|
||||
if err != nil {
|
||||
t.Fatalf("GeneratePassword() returned error: %v", err)
|
||||
|
|
@ -194,7 +194,7 @@ func TestGeneratePasswordReplacements(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run("verifies character replacements", func(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
password, err := GeneratePassword()
|
||||
if err != nil {
|
||||
t.Fatalf("GeneratePassword() returned error: %v", err)
|
||||
|
|
@ -210,7 +210,7 @@ func TestGeneratePasswordReplacements(t *testing.T) {
|
|||
}
|
||||
|
||||
func BenchmarkGeneratePassword(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
_, err := GeneratePassword()
|
||||
if err != nil {
|
||||
b.Fatalf("GeneratePassword() returned error: %v", err)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func GenUsername(email string) (toName string) {
|
|||
toName = email[0:strings.Index(email, "@")]
|
||||
}
|
||||
if len(toName) > 1 {
|
||||
toNameCopy := strings.Replace(toName, ".", " ", -1)
|
||||
toNameCopy := strings.ReplaceAll(toName, ".", " ")
|
||||
toName = ""
|
||||
lastRuneIsSpace := true
|
||||
for _, runeValue := range toNameCopy {
|
||||
|
|
|
|||
36
internal/mailer/log.go
Normal file
36
internal/mailer/log.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/mail"
|
||||
)
|
||||
|
||||
// LogMailer is a dummy mailer that prints emails to stdout.
|
||||
// It is used when no real mail transport is configured.
|
||||
type LogMailer struct{}
|
||||
|
||||
func (l *LogMailer) SendMail(to *mail.Address, subject, content string) error {
|
||||
log.Printf("--- Mail to %s ---\nSubject: %s\n\n%s\n--- End of mail ---", to.String(), subject, content)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -70,13 +70,13 @@ func (r *Mailer) SendMail(to *mail.Address, subject, content string) (err error)
|
|||
"Content": content,
|
||||
}
|
||||
|
||||
if t, err := template.New("mailText").Parse(mailTXTTpl); err != nil {
|
||||
t, err := template.New("mailText").Parse(mailTXTTpl)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.SetBodyWriter("text/plain", func(w io.Writer) error {
|
||||
return t.Execute(w, tplData)
|
||||
})
|
||||
}
|
||||
m.SetBodyWriter("text/plain", func(w io.Writer) error {
|
||||
return t.Execute(w, tplData)
|
||||
})
|
||||
|
||||
// Convert text from Markdown to HTML
|
||||
md := goldmark.New(
|
||||
|
|
@ -95,18 +95,18 @@ func (r *Mailer) SendMail(to *mail.Address, subject, content string) (err error)
|
|||
return
|
||||
}
|
||||
|
||||
if data, err := web.GetEmbedFS().Open("build/img/happydomain.png"); err == nil {
|
||||
if data, imgErr := web.GetEmbedFS().Open("build/img/happydomain.png"); imgErr == nil {
|
||||
m.EmbedReader("happydomain.png", data)
|
||||
}
|
||||
|
||||
if t, err := template.New("mailHTML").Parse(mailHTMLTpl); err != nil {
|
||||
t, err = template.New("mailHTML").Parse(mailHTMLTpl)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.AddAlternativeWriter("text/html", func(w io.Writer) error {
|
||||
tplData["Content"] = buf.String()
|
||||
return t.Execute(w, tplData)
|
||||
})
|
||||
}
|
||||
m.AddAlternativeWriter("text/html", func(w io.Writer) error {
|
||||
tplData["Content"] = buf.String()
|
||||
return t.Execute(w, tplData)
|
||||
})
|
||||
|
||||
if err = r.SendMethod.PrepareAndSend(m); err != nil {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func (t *SystemSendmail) Send(from string, to []string, msg io.WriterTo) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Send sends an e-mail to the given recipients using the sendmail command.
|
||||
// PrepareAndSend sends an e-mail to the given recipients using the sendmail command.
|
||||
func (t *SystemSendmail) PrepareAndSend(m ...*gomail.Message) (err error) {
|
||||
err = gomail.Send(t, m...)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func (t *SMTPMailer) WithTLSNoVerify() {
|
|||
}
|
||||
}
|
||||
|
||||
// Send sends an e-mail to the given recipients using configured SMTP host.
|
||||
// PrepareAndSend sends an e-mail to the given recipients using configured SMTP host.
|
||||
func (t *SMTPMailer) PrepareAndSend(m ...*gomail.Message) (err error) {
|
||||
err = t.Dialer.DialAndSend(m...)
|
||||
|
||||
|
|
|
|||
|
|
@ -36,20 +36,20 @@ import (
|
|||
|
||||
type ListmonkNewsletterSubscription struct {
|
||||
ListmonkURL *url.URL
|
||||
ListmonkId int
|
||||
ListmonkID int
|
||||
}
|
||||
|
||||
type ListmonkSubscriber struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Lists []int `json:"lists"`
|
||||
Attribs map[string]interface{} `json:"attribs,omitempty"`
|
||||
PreconfirmSubscriptions bool `json:"preconfirm_subscriptions,omitempty"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Lists []int `json:"lists"`
|
||||
Attribs map[string]any `json:"attribs,omitempty"`
|
||||
PreconfirmSubscriptions bool `json:"preconfirm_subscriptions,omitempty"`
|
||||
}
|
||||
|
||||
func (ns *ListmonkNewsletterSubscription) SubscribeToNewsletter(u happydns.UserInfo) error {
|
||||
if ns.ListmonkId == 0 {
|
||||
if ns.ListmonkID == 0 {
|
||||
log.Println("SubscribeToNewsletter: not subscribing user as newsletter list id is not defined.")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ func (ns *ListmonkNewsletterSubscription) SubscribeToNewsletter(u happydns.UserI
|
|||
Email: u.GetEmail(),
|
||||
Name: helpers.GenUsername(u.GetEmail()),
|
||||
Status: "enabled",
|
||||
Lists: []int{ns.ListmonkId},
|
||||
Lists: []int{ns.ListmonkID},
|
||||
PreconfirmSubscriptions: true,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func NewSessionStore(opts *happydns.Options, storage sessionUC.SessionStorage, k
|
|||
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
||||
options: &sessions.Options{
|
||||
Path: opts.BasePath + "/",
|
||||
MaxAge: 86400 * 30,
|
||||
MaxAge: int(sessionUC.SESSION_MAX_DURATION.Seconds()),
|
||||
Secure: opts.DevProxy == "" && opts.ExternalURL.Scheme != "http",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
|
|
@ -75,9 +75,13 @@ func (s *SessionStore) New(r *http.Request, name string) (*sessions.Session, err
|
|||
|
||||
if _, ok := r.Header["Authorization"]; ok && len(r.Header["Authorization"]) > 0 {
|
||||
if flds := strings.Fields(r.Header["Authorization"][0]); len(flds) == 2 && flds[0] == "Bearer" {
|
||||
session.ID = flds[1]
|
||||
if isValidSessionID(flds[1]) {
|
||||
session.ID = flds[1]
|
||||
}
|
||||
} else if user, _, ok := r.BasicAuth(); ok {
|
||||
session.ID = user
|
||||
if isValidSessionID(user) {
|
||||
session.ID = user
|
||||
}
|
||||
}
|
||||
} else if cookie, err := r.Cookie(name); err == nil {
|
||||
err := securecookie.DecodeMulti(name, cookie.Value, &session.ID, s.Codecs...)
|
||||
|
|
@ -93,7 +97,9 @@ func (s *SessionStore) New(r *http.Request, name string) (*sessions.Session, err
|
|||
}
|
||||
|
||||
err := s.load(session)
|
||||
session.IsNew = false
|
||||
if err == nil {
|
||||
session.IsNew = false
|
||||
}
|
||||
return session, err
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +108,9 @@ func (s *SessionStore) Save(r *http.Request, w http.ResponseWriter, session *ses
|
|||
var cookieValue string
|
||||
|
||||
if s.options.MaxAge < 0 || session.Options.MaxAge < 0 {
|
||||
s.storage.DeleteSession(session.ID)
|
||||
if err := s.storage.DeleteSession(session.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if session.ID == "" {
|
||||
session.ID = sessionUC.NewSessionID()
|
||||
|
|
@ -163,6 +171,11 @@ func (s *SessionStore) load(session *sessions.Session) error {
|
|||
session.Values["created_on"] = mysession.IssuedAt
|
||||
}
|
||||
if !mysession.ExpiresOn.IsZero() {
|
||||
if mysession.ExpiresOn.Before(time.Now()) {
|
||||
// Session has expired; delete it and treat this as a new session.
|
||||
_ = s.storage.DeleteSession(session.ID)
|
||||
return fmt.Errorf("session has expired")
|
||||
}
|
||||
session.Values["expires_on"] = mysession.ExpiresOn
|
||||
}
|
||||
|
||||
|
|
@ -202,11 +215,12 @@ func (s *SessionStore) save(session *sessions.Session, ua string) error {
|
|||
}
|
||||
|
||||
if exOn == nil {
|
||||
expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
|
||||
expiresOn = time.Now().Add(sessionUC.SESSION_MAX_DURATION)
|
||||
} else {
|
||||
expiresOn = exOn.(time.Time)
|
||||
if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 {
|
||||
expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
|
||||
// Auto-renew if the session expires within the renewal window.
|
||||
if time.Until(expiresOn) < sessionUC.SESSION_RENEWAL_THRESHOLD {
|
||||
expiresOn = time.Now().Add(sessionUC.SESSION_MAX_DURATION)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -227,3 +241,17 @@ func (s *SessionStore) save(session *sessions.Session, ua string) error {
|
|||
|
||||
return s.storage.UpdateSession(mysession)
|
||||
}
|
||||
|
||||
// isValidSessionID returns true if s looks like a session ID generated by
|
||||
// NewSessionID: base32 standard alphabet ([A-Z2-7]), exactly 103 characters.
|
||||
func isValidSessionID(s string) bool {
|
||||
if len(s) != 103 {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
if !((c >= 'A' && c <= 'Z') || (c >= '2' && c <= '7')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ func (s *InMemoryStorage) Close() error {
|
|||
}
|
||||
|
||||
// DecodeData decodes data from the interface (expected to be []byte) into v.
|
||||
func (s *InMemoryStorage) DecodeData(data interface{}, v interface{}) error {
|
||||
func (s *InMemoryStorage) DecodeData(data any, v any) error {
|
||||
b, ok := data.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("data to decode are not in []byte format (%T)", data)
|
||||
|
|
@ -111,7 +111,7 @@ func (s *InMemoryStorage) Has(key string) (bool, error) {
|
|||
}
|
||||
|
||||
// Get retrieves a value by key and decodes it into v.
|
||||
func (s *InMemoryStorage) Get(key string, v interface{}) error {
|
||||
func (s *InMemoryStorage) Get(key string, v any) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
data, exists := s.data[key]
|
||||
|
|
@ -122,7 +122,7 @@ func (s *InMemoryStorage) Get(key string, v interface{}) error {
|
|||
}
|
||||
|
||||
// Put stores a value with the given key.
|
||||
func (s *InMemoryStorage) Put(key string, v interface{}) error {
|
||||
func (s *InMemoryStorage) Put(key string, v any) error {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func (it *KVIterator) Key() string {
|
|||
}
|
||||
|
||||
// Value returns the current value.
|
||||
func (it *KVIterator) Value() interface{} {
|
||||
func (it *KVIterator) Value() any {
|
||||
if it.index < 0 || it.index >= len(it.keys) {
|
||||
return []byte{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,15 +63,15 @@ type Iterator interface {
|
|||
Next() bool
|
||||
Valid() bool
|
||||
Key() string
|
||||
Value() interface{}
|
||||
Value() any
|
||||
}
|
||||
|
||||
type KVStorage interface {
|
||||
Close() error
|
||||
DecodeData(i interface{}, v interface{}) error
|
||||
DecodeData(i any, v any) error
|
||||
Has(key string) (bool, error)
|
||||
Get(key string, v interface{}) error
|
||||
Put(key string, v interface{}) error
|
||||
Get(key string, v any) error
|
||||
Put(key string, v any) error
|
||||
FindIdentifierKey(prefix string) (key string, id happydns.Identifier, err error)
|
||||
Delete(key string) error
|
||||
Search(prefix string) Iterator
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ func (it *KVIterator[T]) DropItem() error {
|
|||
|
||||
// Raw returns the raw (non-decoded) value at the current iterator position.
|
||||
// Should only be called after a successful call to Next().
|
||||
func (it *KVIterator[T]) Raw() interface{} {
|
||||
func (it *KVIterator[T]) Raw() any {
|
||||
if it.iter == nil || !it.iter.Valid() {
|
||||
return []byte{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ import (
|
|||
// abstract.EMail
|
||||
func explodeAbstractEMail(dn happydns.Subdomain, in *happydns.ServiceMessage) ([]*happydns.ServiceMessage, error) {
|
||||
var val struct {
|
||||
MX []map[string]interface{} `json:"mx,omitempty"`
|
||||
SPF map[string]interface{} `json:"spf,omitempty"`
|
||||
MX []map[string]any `json:"mx,omitempty"`
|
||||
SPF map[string]any `json:"spf,omitempty"`
|
||||
DKIM map[string]*svcs.DKIM `json:"dkim,omitempty"`
|
||||
DMARC *svcs.DMARCFields `json:"dmarc,omitempty"`
|
||||
MTA_STS *svcs.MTASTSFields `json:"mta_sts,omitempty"`
|
||||
|
|
@ -56,11 +56,12 @@ func explodeAbstractEMail(dn happydns.Subdomain, in *happydns.ServiceMessage) ([
|
|||
if len(val.MX) > 0 {
|
||||
var mxs svcs.MXs
|
||||
|
||||
var rr dns.RR
|
||||
for _, mx := range val.MX {
|
||||
if _, ok := mx["preference"]; !ok {
|
||||
mx["preference"] = 0.0
|
||||
}
|
||||
rr, err := dns.NewRR(fmt.Sprintf("zZzZ. 0 IN MX %.0f %s", mx["preference"].(float64), helpers.DomainFQDN(mx["target"].(string), "zZzZ.")))
|
||||
rr, err = dns.NewRR(fmt.Sprintf("zZzZ. 0 IN MX %.0f %s", mx["preference"].(float64), helpers.DomainFQDN(mx["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
|
@ -82,8 +83,8 @@ func explodeAbstractEMail(dn happydns.Subdomain, in *happydns.ServiceMessage) ([
|
|||
}
|
||||
|
||||
if val.SPF != nil {
|
||||
if _, ok := val.SPF["directives"].([]interface{}); ok {
|
||||
directives := val.SPF["directives"].([]interface{})
|
||||
if _, ok := val.SPF["directives"].([]any); ok {
|
||||
directives := val.SPF["directives"].([]any)
|
||||
var dir []string
|
||||
for _, directive := range directives {
|
||||
dir = append(dir, directive.(string))
|
||||
|
|
@ -191,12 +192,12 @@ func explodeAbstractEMail(dn happydns.Subdomain, in *happydns.ServiceMessage) ([
|
|||
|
||||
var migrateFrom7SvcType map[string]func(json.RawMessage) (json.RawMessage, error)
|
||||
|
||||
func migrateFrom7(s *KVStorage) (err error) {
|
||||
func migrateFrom7(s *KVStorage) error {
|
||||
migrateFrom7SvcType = make(map[string]func(json.RawMessage) (json.RawMessage, error))
|
||||
|
||||
// abstract.ACMEChallenge
|
||||
migrateFrom7SvcType["abstract.ACMEChallenge"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -218,7 +219,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.Delegation
|
||||
migrateFrom7SvcType["abstract.Delegation"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -227,7 +228,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
var delegation abstract.Delegation
|
||||
|
||||
if nss, ok := val["ns"].([]interface{}); ok {
|
||||
if nss, ok := val["ns"].([]any); ok {
|
||||
for _, ns := range nss {
|
||||
rr, err := dns.NewRR(fmt.Sprintf("zZzZ. 0 IN NS %s", helpers.DomainFQDN(ns.(string), "zZzZ")))
|
||||
if err != nil {
|
||||
|
|
@ -239,9 +240,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if dss, ok := val["ds"].([]interface{}); ok {
|
||||
if dss, ok := val["ds"].([]any); ok {
|
||||
for _, dsI := range dss {
|
||||
ds := dsI.(map[string]interface{})
|
||||
ds := dsI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("zZzZ. 0 IN DS %.0f %.0f %.0f %s", ds["keytag"].(float64), ds["algorithm"].(float64), ds["digestType"].(float64), ds["digest"].(string)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -257,7 +258,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.GithubOrgVerif
|
||||
migrateFrom7SvcType["abstract.GithubOrgVerif"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -279,7 +280,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.GitlabPageVerif
|
||||
migrateFrom7SvcType["abstract.GitlabPageVerif"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -301,7 +302,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.GoogleVerif
|
||||
migrateFrom7SvcType["abstract.GoogleVerif"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -323,7 +324,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.KeybaseVerif
|
||||
migrateFrom7SvcType["abstract.KeybaseVerif"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -345,7 +346,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.MatrixIM
|
||||
migrateFrom7SvcType["abstract.MatrixIM"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -354,9 +355,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
var matrix abstract.MatrixIM
|
||||
|
||||
if mat, ok := val["matrix"].([]interface{}); ok {
|
||||
if mat, ok := val["matrix"].([]any); ok {
|
||||
for _, mI := range mat {
|
||||
m := mI.(map[string]interface{})
|
||||
m := mI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("_matrix._tcp.zZzZ. 0 IN SRV %.0f %.0f %.0f %s", m["priority"].(float64), m["weight"].(float64), m["port"].(float64), helpers.DomainFQDN(m["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -372,7 +373,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.OpenPGP
|
||||
migrateFrom7SvcType["abstract.OpenPGP"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -397,7 +398,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.SMimeCert
|
||||
migrateFrom7SvcType["abstract.SMimeCert"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -422,7 +423,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.Origin
|
||||
migrateFrom7SvcType["abstract.Origin"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -439,8 +440,8 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
origin.SOA = helpers.RRRelative(rr, "zZzZ").(*dns.SOA)
|
||||
}
|
||||
|
||||
if _, ok := val["ns"].([]interface{}); ok {
|
||||
for _, nsI := range val["ns"].([]interface{}) {
|
||||
if _, ok := val["ns"].([]any); ok {
|
||||
for _, nsI := range val["ns"].([]any) {
|
||||
rr, err := dns.NewRR(fmt.Sprintf("zZzZ. 0 IN NS %s", helpers.DomainFQDN(nsI.(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -456,7 +457,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.NSOnlyOrigin
|
||||
migrateFrom7SvcType["abstract.NSOnlyOrigin"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -465,8 +466,8 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
var origin abstract.NSOnlyOrigin
|
||||
|
||||
if _, ok := val["ns"].([]interface{}); ok {
|
||||
for _, nsI := range val["ns"].([]interface{}) {
|
||||
if _, ok := val["ns"].([]any); ok {
|
||||
for _, nsI := range val["ns"].([]any) {
|
||||
rr, err := dns.NewRR(fmt.Sprintf("zZzZ. 0 IN NS %s", helpers.DomainFQDN(nsI.(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -482,7 +483,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.RFC6186
|
||||
migrateFrom7SvcType["abstract.RFC6186"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -491,9 +492,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
var rfc6186 abstract.RFC6186
|
||||
|
||||
if _, ok := val["submission"].([]interface{}); ok {
|
||||
for _, clientI := range val["submission"].([]interface{}) {
|
||||
client := clientI.(map[string]interface{})
|
||||
if _, ok := val["submission"].([]any); ok {
|
||||
for _, clientI := range val["submission"].([]any) {
|
||||
client := clientI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s.zZzZ. 0 IN SRV %.0f %.0f %.0f %s", helpers.DomainJoin("_submission", "_tcp"), client["priority"].(float64), client["weight"].(float64), client["port"].(float64), helpers.DomainFQDN(client["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -503,9 +504,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if _, ok := val["imaps"].([]interface{}); ok {
|
||||
for _, clientI := range val["imaps"].([]interface{}) {
|
||||
client := clientI.(map[string]interface{})
|
||||
if _, ok := val["imaps"].([]any); ok {
|
||||
for _, clientI := range val["imaps"].([]any) {
|
||||
client := clientI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s.zZzZ. 0 IN SRV %.0f %.0f %.0f %s", helpers.DomainJoin("_imaps", "_tcp"), client["priority"].(float64), client["weight"].(float64), client["port"].(float64), helpers.DomainFQDN(client["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -515,9 +516,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if _, ok := val["pop3s"].([]interface{}); ok {
|
||||
for _, clientI := range val["pop3s"].([]interface{}) {
|
||||
client := clientI.(map[string]interface{})
|
||||
if _, ok := val["pop3s"].([]any); ok {
|
||||
for _, clientI := range val["pop3s"].([]any) {
|
||||
client := clientI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s.zZzZ. 0 IN SRV %.0f %.0f %.0f %s", helpers.DomainJoin("_pop3s", "_tcp"), client["priority"].(float64), client["weight"].(float64), client["port"].(float64), helpers.DomainFQDN(client["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -527,9 +528,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if _, ok := val["submissions"].([]interface{}); ok {
|
||||
for _, clientI := range val["submissions"].([]interface{}) {
|
||||
client := clientI.(map[string]interface{})
|
||||
if _, ok := val["submissions"].([]any); ok {
|
||||
for _, clientI := range val["submissions"].([]any) {
|
||||
client := clientI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s.zZzZ. 0 IN SRV %.0f %.0f %.0f %s", helpers.DomainJoin("_submissions", "_tcp"), client["priority"].(float64), client["weight"].(float64), client["port"].(float64), helpers.DomainFQDN(client["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -539,9 +540,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if _, ok := val["imap"].([]interface{}); ok {
|
||||
for _, clientI := range val["imap"].([]interface{}) {
|
||||
client := clientI.(map[string]interface{})
|
||||
if _, ok := val["imap"].([]any); ok {
|
||||
for _, clientI := range val["imap"].([]any) {
|
||||
client := clientI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s.zZzZ. 0 IN SRV %.0f %.0f %.0f %s", helpers.DomainJoin("_imap", "_tcp"), client["priority"].(float64), client["weight"].(float64), client["port"].(float64), helpers.DomainFQDN(client["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -551,9 +552,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if _, ok := val["pop3"].([]interface{}); ok {
|
||||
for _, clientI := range val["pop3"].([]interface{}) {
|
||||
client := clientI.(map[string]interface{})
|
||||
if _, ok := val["pop3"].([]any); ok {
|
||||
for _, clientI := range val["pop3"].([]any) {
|
||||
client := clientI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s.zZzZ. 0 IN SRV %.0f %.0f %.0f %s", helpers.DomainJoin("_pop3", "_tcp"), client["priority"].(float64), client["weight"].(float64), client["port"].(float64), helpers.DomainFQDN(client["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -569,7 +570,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.ScalewayChallenge
|
||||
migrateFrom7SvcType["abstract.ScalewayChallenge"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -591,7 +592,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.Server
|
||||
migrateFrom7SvcType["abstract.Server"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -620,9 +621,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if _, ok := val["SSHFP"].([]interface{}); ok {
|
||||
for _, sshfpI := range val["SSHFP"].([]interface{}) {
|
||||
sshfp := sshfpI.(map[string]interface{})
|
||||
if _, ok := val["SSHFP"].([]any); ok {
|
||||
for _, sshfpI := range val["SSHFP"].([]any) {
|
||||
sshfp := sshfpI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("zZzZ. 0 IN SSHFP %.0f %.0f %s", sshfp["algorithm"], sshfp["type"], sshfp["fingerprint"]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -638,7 +639,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// abstract.XMPP
|
||||
migrateFrom7SvcType["abstract.XMPP"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -647,9 +648,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
var xmpp abstract.XMPP
|
||||
|
||||
if _, ok := val["Client"].([]interface{}); ok {
|
||||
for _, clientI := range val["Client"].([]interface{}) {
|
||||
client := clientI.(map[string]interface{})
|
||||
if _, ok := val["Client"].([]any); ok {
|
||||
for _, clientI := range val["Client"].([]any) {
|
||||
client := clientI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("_xmpp-client._tcp.zZzZ. 0 IN SRV %.0f %.0f %.0f %s", client["priority"], client["weight"], client["port"], helpers.DomainFQDN(client["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -660,9 +661,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if _, ok := val["Server"].([]interface{}); ok {
|
||||
for _, serverI := range val["Server"].([]interface{}) {
|
||||
server := serverI.(map[string]interface{})
|
||||
if _, ok := val["Server"].([]any); ok {
|
||||
for _, serverI := range val["Server"].([]any) {
|
||||
server := serverI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("_xmpp-server._tcp.zZzZ. 0 IN SRV %.0f %.0f %.0f %s", server["priority"], server["weight"], server["port"], helpers.DomainFQDN(server["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -673,9 +674,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if _, ok := val["Jabber"].([]interface{}); ok {
|
||||
for _, jabberI := range val["Jabber"].([]interface{}) {
|
||||
jabber := jabberI.(map[string]interface{})
|
||||
if _, ok := val["Jabber"].([]any); ok {
|
||||
for _, jabberI := range val["Jabber"].([]any) {
|
||||
jabber := jabberI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("_jabber._tcp.zZzZ. 0 IN SRV %.0f %.0f %.0f %s", jabber["priority"], jabber["weight"], jabber["port"], helpers.DomainFQDN(jabber["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -841,7 +842,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// svcs.MXs
|
||||
migrateFrom7SvcType["svcs.MXs"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -850,8 +851,8 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
var mxs svcs.MXs
|
||||
|
||||
for _, mxI := range val["mx"].([]interface{}) {
|
||||
mx := mxI.(map[string]interface{})
|
||||
for _, mxI := range val["mx"].([]any) {
|
||||
mx := mxI.(map[string]any)
|
||||
if _, ok := mx["preference"]; !ok {
|
||||
mx["preference"] = 0.0
|
||||
}
|
||||
|
|
@ -868,14 +869,14 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// svcs.SPF
|
||||
migrateFrom7SvcType["svcs.SPF"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
directives := val["directives"].([]interface{})
|
||||
directives := val["directives"].([]any)
|
||||
var dir []string
|
||||
for _, directive := range directives {
|
||||
dir = append(dir, directive.(string))
|
||||
|
|
@ -891,7 +892,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// svcs.TLSAs
|
||||
migrateFrom7SvcType["svcs.TLSAs"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -900,8 +901,8 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
var tlsa svcs.TLSAs
|
||||
|
||||
for _, tlsaI := range val["tlsa"].([]interface{}) {
|
||||
t := tlsaI.(map[string]interface{})
|
||||
for _, tlsaI := range val["tlsa"].([]any) {
|
||||
t := tlsaI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s 0 IN TLSA %.0f %.0f %.0f %s", helpers.DomainJoin(fmt.Sprintf("_%.0f._%s.zZzZ.", t["port"].(float64), t["proto"].(string))), t["certusage"].(float64), t["selector"].(float64), t["matchingtype"].(float64), t["certificate"].(string)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -967,7 +968,7 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
// svcs.UnknownSRV
|
||||
migrateFrom7SvcType["svcs.UnknownSRV"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
@ -976,9 +977,9 @@ func migrateFrom7(s *KVStorage) (err error) {
|
|||
|
||||
var usrv svcs.UnknownSRV
|
||||
|
||||
if mat, ok := val["srv"].([]interface{}); ok {
|
||||
if mat, ok := val["srv"].([]any); ok {
|
||||
for _, mI := range mat {
|
||||
m := mI.(map[string]interface{})
|
||||
m := mI.(map[string]any)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s 0 IN SRV %.0f %.0f %.0f %s", helpers.DomainJoin("_"+val["name"].(string), "_"+val["proto"].(string), "zZzZ"), m["priority"].(float64), m["weight"].(float64), m["port"].(float64), helpers.DomainFQDN(m["target"].(string), "zZzZ.")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ import (
|
|||
"git.happydns.org/happyDomain/services/providers/google"
|
||||
)
|
||||
|
||||
func migrateFrom8(s *KVStorage) (err error) {
|
||||
func migrateFrom8(s *KVStorage) error {
|
||||
migrateFrom7SvcType = make(map[string]func(json.RawMessage) (json.RawMessage, error))
|
||||
|
||||
// google.GSuite
|
||||
migrateFrom7SvcType["google.GSuite"] = func(in json.RawMessage) (json.RawMessage, error) {
|
||||
val := map[string]interface{}{}
|
||||
val := map[string]any{}
|
||||
|
||||
err := json.Unmarshal(in, &val)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -65,11 +65,11 @@ func (s *LevelDBStorage) Close() error {
|
|||
return s.db.Close()
|
||||
}
|
||||
|
||||
func decodeData(data []byte, v interface{}) error {
|
||||
func decodeData(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorage) DecodeData(data interface{}, v interface{}) error {
|
||||
func (s *LevelDBStorage) DecodeData(data any, v any) error {
|
||||
b, ok := data.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("data to decode are not in []byte format (%T)", data)
|
||||
|
|
@ -81,7 +81,7 @@ func (s *LevelDBStorage) Has(key string) (bool, error) {
|
|||
return s.db.Has([]byte(key), nil)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorage) Get(key string, v interface{}) error {
|
||||
func (s *LevelDBStorage) Get(key string, v any) error {
|
||||
data, err := s.db.Get([]byte(key), nil)
|
||||
if err != nil {
|
||||
if goerrors.Is(err, leveldb.ErrNotFound) {
|
||||
|
|
@ -93,7 +93,7 @@ func (s *LevelDBStorage) Get(key string, v interface{}) error {
|
|||
return decodeData(data, v)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorage) Put(key string, v interface{}) error {
|
||||
func (s *LevelDBStorage) Put(key string, v any) error {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ func (it *LevelDBIterator) Key() string {
|
|||
return string(it.iter.Key())
|
||||
}
|
||||
|
||||
func (it *LevelDBIterator) Value() interface{} {
|
||||
func (it *LevelDBIterator) Value() any {
|
||||
return it.iter.Value()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,11 +78,11 @@ func (s *NoSQLStorage) Close() error {
|
|||
return s.client.Close()
|
||||
}
|
||||
|
||||
func (n *NoSQLStorage) DecodeData(data interface{}, v interface{}) error {
|
||||
func (n *NoSQLStorage) DecodeData(data any, v any) error {
|
||||
return json.Unmarshal([]byte(jsonutil.AsJSON(data)), v)
|
||||
}
|
||||
|
||||
func (n *NoSQLStorage) Get(key string, v interface{}) error {
|
||||
func (n *NoSQLStorage) Get(key string, v any) error {
|
||||
gkey := &types.MapValue{}
|
||||
gkey.Put("key", key)
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ func (n *NoSQLStorage) Get(key string, v interface{}) error {
|
|||
return n.DecodeData(data, v)
|
||||
}
|
||||
|
||||
func (n *NoSQLStorage) Put(key string, v interface{}) error {
|
||||
func (n *NoSQLStorage) Put(key string, v any) error {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal data: %w", err)
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ func (i *Iterator) Key() string {
|
|||
return key.(string)
|
||||
}
|
||||
|
||||
func (i *Iterator) Value() interface{} {
|
||||
func (i *Iterator) Value() any {
|
||||
value, _ := i.results[i.cur_result].Get("value")
|
||||
return value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ func (s *PostgreSQLStorage) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *PostgreSQLStorage) DecodeData(data interface{}, v interface{}) error {
|
||||
func (s *PostgreSQLStorage) DecodeData(data any, v any) error {
|
||||
var bytes []byte
|
||||
|
||||
switch d := data.(type) {
|
||||
|
|
@ -146,7 +146,7 @@ func (s *PostgreSQLStorage) Has(key string) (bool, error) {
|
|||
return exists, nil
|
||||
}
|
||||
|
||||
func (s *PostgreSQLStorage) Get(key string, v interface{}) error {
|
||||
func (s *PostgreSQLStorage) Get(key string, v any) error {
|
||||
query := fmt.Sprintf("SELECT data FROM %s WHERE key = $1", s.table)
|
||||
|
||||
var jsonData []byte
|
||||
|
|
@ -161,7 +161,7 @@ func (s *PostgreSQLStorage) Get(key string, v interface{}) error {
|
|||
return json.Unmarshal(jsonData, v)
|
||||
}
|
||||
|
||||
func (s *PostgreSQLStorage) Put(key string, v interface{}) error {
|
||||
func (s *PostgreSQLStorage) Put(key string, v any) error {
|
||||
// Marshal value to JSON
|
||||
jsonData, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -86,6 +86,6 @@ func (it *PostgreSQLIterator) Key() string {
|
|||
}
|
||||
|
||||
// Value returns the current value as []byte
|
||||
func (it *PostgreSQLIterator) Value() interface{} {
|
||||
func (it *PostgreSQLIterator) Value() any {
|
||||
return it.value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func (i *StorageEngine) String() string {
|
|||
|
||||
func (i *StorageEngine) Set(value string) (err error) {
|
||||
if _, ok := StorageEngines[value]; !ok {
|
||||
return fmt.Errorf("Unexistant storage engine: please select one between: %v", GetStorageEngines())
|
||||
return fmt.Errorf("unexistant storage engine: please select one between: %v", GetStorageEngines())
|
||||
}
|
||||
*i = StorageEngine(value)
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ func Test_AuthenticateUserWithPassword_WrongPassword(t *testing.T) {
|
|||
Email: "a@b.c",
|
||||
Password: "wrong-password",
|
||||
})
|
||||
if err == nil || err.Error() != `tries to login as "a@b.c", but sent an invalid password` {
|
||||
if err == nil || err.Error() != `invalid password` {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@ func Test_AuthenticateUserWithPassword_WeakPassword(t *testing.T) {
|
|||
Email: "a@b.c",
|
||||
Password: "weak",
|
||||
})
|
||||
if err == nil || err.Error() != `tries to login as "a@b.c", but sent an invalid password` {
|
||||
if err == nil || err.Error() != `invalid password` {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -180,7 +180,7 @@ func Test_AuthenticateUserWithPassword_UnverifiedEmail(t *testing.T) {
|
|||
Email: "a@b.c",
|
||||
Password: "v3rySecure",
|
||||
})
|
||||
if err == nil || err.Error() != `tries to login as "a@b.c", but has not verified email` {
|
||||
if err == nil || err.Error() != `account email not verified` {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/usecase/authuser"
|
||||
"git.happydns.org/happyDomain/internal/usecase/user"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
|
|
@ -77,17 +79,28 @@ func (lu *loginUsecase) AuthenticateUserWithPassword(request happydns.LoginReque
|
|||
// Retrieve the given user
|
||||
user, err := lu.store.GetAuthUserByEmail(request.Email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user's email (%s) not found: %s", request.Email, err.Error())
|
||||
// Perform a dummy bcrypt comparison to equalize timing with the
|
||||
// valid-user path and prevent email enumeration via response time.
|
||||
bcrypt.CompareHashAndPassword([]byte("$2a$12$dummy.hash.that.never.matches.any.real.password.value"), []byte(request.Password))
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
if !user.CheckPassword(request.Password) {
|
||||
return nil, fmt.Errorf("tries to login as %q, but sent an invalid password", request.Email)
|
||||
return nil, fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
// Ensure the account is enabled
|
||||
if !lu.config.NoMail && user.EmailVerification == nil {
|
||||
return nil, fmt.Errorf("tries to login as %q, but has not verified email", request.Email)
|
||||
return nil, fmt.Errorf("account email not verified")
|
||||
}
|
||||
|
||||
// Record the successful login time and transparently upgrade the hash cost if needed
|
||||
now := time.Now()
|
||||
user.LastLoggedIn = &now
|
||||
if user.NeedsRehash() {
|
||||
user.DefinePassword(request.Password)
|
||||
}
|
||||
lu.store.UpdateAuthUser(user)
|
||||
|
||||
return lu.CompleteAuthentication(user)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,11 @@ package authuser
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"log"
|
||||
"net/mail"
|
||||
"unicode"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
|
|
@ -89,14 +90,31 @@ func (s *Service) checkPasswordConstraints(password, confirmation string) error
|
|||
if len(password) < 8 {
|
||||
return happydns.ValidationError{Msg: "password must be at least 8 characters long"}
|
||||
}
|
||||
if len(password) > 72 {
|
||||
return happydns.ValidationError{Msg: "password must be at most 72 characters long"}
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`[a-z]`).MatchString(password) {
|
||||
var hasLower, hasUpper, hasDigit, hasSymbol bool
|
||||
for _, r := range password {
|
||||
switch {
|
||||
case unicode.IsLower(r):
|
||||
hasLower = true
|
||||
case unicode.IsUpper(r):
|
||||
hasUpper = true
|
||||
case unicode.IsDigit(r):
|
||||
hasDigit = true
|
||||
default:
|
||||
hasSymbol = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasLower {
|
||||
return happydns.ValidationError{Msg: "Password must contain lower case letters."}
|
||||
} else if !regexp.MustCompile(`[A-Z]`).MatchString(password) {
|
||||
} else if !hasUpper {
|
||||
return happydns.ValidationError{Msg: "Password must contain upper case letters."}
|
||||
} else if !regexp.MustCompile(`[0-9]`).MatchString(password) {
|
||||
} else if !hasDigit {
|
||||
return happydns.ValidationError{Msg: "Password must contain numbers."}
|
||||
} else if len(password) < 11 && !regexp.MustCompile(`[^a-zA-Z0-9]`).MatchString(password) {
|
||||
} else if len(password) < 11 && !hasSymbol {
|
||||
return happydns.ValidationError{Msg: "Password must be longer or contain symbols."}
|
||||
}
|
||||
|
||||
|
|
@ -128,9 +146,11 @@ func (s *Service) ChangePassword(user *happydns.UserAuth, newPassword string) er
|
|||
}
|
||||
|
||||
// CreateAuthUser validates the registration request, creates the user, and optionally sends a validation email.
|
||||
// To prevent user enumeration, this method returns nil user with nil error when an account
|
||||
// already exists with the given email address, after sending a notification to the existing user.
|
||||
func (s *Service) CreateAuthUser(uu happydns.UserRegistration) (*happydns.UserAuth, error) {
|
||||
// Validate email format
|
||||
if len(uu.Email) <= 3 || !strings.Contains(uu.Email, "@") {
|
||||
if _, err := mail.ParseAddress(uu.Email); err != nil {
|
||||
return nil, happydns.ValidationError{Msg: "the given email is invalid"}
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +169,9 @@ func (s *Service) CreateAuthUser(uu happydns.UserRegistration) (*happydns.UserAu
|
|||
}
|
||||
}
|
||||
if exists {
|
||||
return nil, happydns.ValidationError{Msg: "an account already exists with the given address. Try logging in."}
|
||||
// Send a notification to the existing user (best effort) to avoid user enumeration.
|
||||
s.sendDuplicateRegistrationNotice(uu.Email)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create the user object
|
||||
|
|
@ -163,26 +185,47 @@ func (s *Service) CreateAuthUser(uu happydns.UserRegistration) (*happydns.UserAu
|
|||
user.AllowCommercials = uu.Newsletter
|
||||
|
||||
// Persist the new user in the storage layer
|
||||
if err := s.store.CreateAuthUser(user); err != nil {
|
||||
if err = s.store.CreateAuthUser(user); err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to create user in storage: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to create your account. Please try again later.",
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally send the validation email if mailer is configured
|
||||
if s.mailer != nil && !reflect.ValueOf(s.mailer).IsNil() {
|
||||
if err = s.emailValidation.SendLink(user); err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to send validation email: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to create your account. Please try again later.",
|
||||
}
|
||||
// Send the validation email
|
||||
if err = s.emailValidation.SendLink(user); err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to send validation email: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to create your account. Please try again later.",
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// sendDuplicateRegistrationNotice sends an email to an existing user when someone
|
||||
// attempts to register with their email address.
|
||||
func (s *Service) sendDuplicateRegistrationNotice(email string) {
|
||||
toName := helpers.GenUsername(email)
|
||||
err := s.mailer.SendMail(
|
||||
&mail.Address{Name: toName, Address: email},
|
||||
"Registration attempt on happyDomain",
|
||||
fmt.Sprintf(`Hi %s,
|
||||
|
||||
Someone (possibly you) attempted to create a new account on happyDomain
|
||||
using your email address.
|
||||
|
||||
If this was you, you already have an account. You can log in or use the
|
||||
password recovery feature if you have forgotten your password.
|
||||
|
||||
If this was not you, you can safely ignore this email.
|
||||
`, toName),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("unable to send duplicate registration notice to %s: %v", email, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAuthUser deletes an authenticated user from the system, ensuring their sessions are also removed.
|
||||
func (s *Service) DeleteAuthUser(user *happydns.UserAuth, password string) error {
|
||||
// Verify the current password
|
||||
|
|
@ -232,7 +275,7 @@ func (s *Service) SendRecoveryLink(user *happydns.UserAuth) error {
|
|||
}
|
||||
|
||||
// GenerateValidationLink generates an email validation link for the given user.
|
||||
func (s *Service) GenerateValidationLink(user *happydns.UserAuth) string {
|
||||
func (s *Service) GenerateValidationLink(user *happydns.UserAuth) (string, error) {
|
||||
return s.emailValidation.GenerateLink(user)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@
|
|||
package authuser_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -33,6 +36,13 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// NoopMailer is a mock mailer that discards all emails.
|
||||
type NoopMailer struct{}
|
||||
|
||||
func (n *NoopMailer) SendMail(to *mail.Address, subject, content string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MockCloseUserSessionsUsecase is a mock implementation of SessionCloserUsecase.
|
||||
type MockCloseUserSessionsUsecase struct {
|
||||
CloseAllFunc func(user happydns.UserInfo) error
|
||||
|
|
@ -56,11 +66,21 @@ func setupTestService() (*authuser.Service, storage.Storage) {
|
|||
DisableRegistration: false,
|
||||
}
|
||||
mockCloseSessions := &MockCloseUserSessionsUsecase{}
|
||||
// Pass nil mailer to avoid sending emails in tests
|
||||
service := authuser.NewAuthUserUsecases(cfg, nil, store, mockCloseSessions)
|
||||
service := authuser.NewAuthUserUsecases(cfg, &NoopMailer{}, store, mockCloseSessions)
|
||||
return service, store
|
||||
}
|
||||
|
||||
func requireValidationError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, got nil")
|
||||
}
|
||||
var ve happydns.ValidationError
|
||||
if !errors.As(err, &ve) {
|
||||
t.Fatalf("expected ValidationError, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CanRegister Tests ==========
|
||||
|
||||
func TestCanRegister_Success(t *testing.T) {
|
||||
|
|
@ -81,10 +101,10 @@ func TestCanRegister_Closed(t *testing.T) {
|
|||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
store, _ := kv.NewKVDatabase(mem)
|
||||
cfg := &happydns.Options{
|
||||
DisableRegistration: true, // Registration closed
|
||||
DisableRegistration: true,
|
||||
}
|
||||
mockCloseSessions := &MockCloseUserSessionsUsecase{}
|
||||
service := authuser.NewAuthUserUsecases(cfg, nil, store, mockCloseSessions)
|
||||
service := authuser.NewAuthUserUsecases(cfg, &NoopMailer{}, store, mockCloseSessions)
|
||||
|
||||
reg := happydns.UserRegistration{
|
||||
Email: "test@example.com",
|
||||
|
|
@ -92,8 +112,8 @@ func TestCanRegister_Closed(t *testing.T) {
|
|||
}
|
||||
|
||||
err := service.CanRegister(reg)
|
||||
if err == nil || err.Error() != "Registration are closed on this instance." {
|
||||
t.Errorf("expected registration closed error, got: %v", err)
|
||||
if err == nil {
|
||||
t.Error("expected registration closed error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +137,7 @@ func TestCreateAuthUser_Success(t *testing.T) {
|
|||
t.Errorf("expected email %s, got %s", reg.Email, user.Email)
|
||||
}
|
||||
if user.Password == nil {
|
||||
t.Errorf("expected defined password, got %s", user.Password)
|
||||
t.Error("expected defined password")
|
||||
}
|
||||
if !user.AllowCommercials {
|
||||
t.Error("expected user to have AllowCommercials = true")
|
||||
|
|
@ -127,53 +147,71 @@ func TestCreateAuthUser_Success(t *testing.T) {
|
|||
func TestCreateAuthUser_InvalidEmail(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
reg := happydns.UserRegistration{
|
||||
Email: "bademail",
|
||||
Password: "StrongPassword123!",
|
||||
}
|
||||
|
||||
_, err := service.CreateAuthUser(reg)
|
||||
if err == nil || err.Error() != "the given email is invalid" {
|
||||
t.Errorf("expected validation error for email, got: %v", err)
|
||||
cases := []string{"", "ab", "bademail", "a@"}
|
||||
for _, email := range cases {
|
||||
t.Run(email, func(t *testing.T) {
|
||||
reg := happydns.UserRegistration{
|
||||
Email: email,
|
||||
Password: "StrongPassword123!",
|
||||
}
|
||||
_, err := service.CreateAuthUser(reg)
|
||||
requireValidationError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAuthUser_WeakPassword(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
password string
|
||||
}{
|
||||
{"too short", "123"},
|
||||
{"short with symbols", "Secur3$"},
|
||||
{"no uppercase", "secure123"},
|
||||
{"short without symbols", "Secure123"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
reg := happydns.UserRegistration{
|
||||
Email: "test@example.com",
|
||||
Password: tc.password,
|
||||
}
|
||||
_, err := service.CreateAuthUser(reg)
|
||||
requireValidationError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAuthUser_PasswordMaxLength(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
// Exactly 72 characters should be accepted (bcrypt limit)
|
||||
pw72 := "Abcdefg1!" + strings.Repeat("x", 63) // 9 + 63 = 72
|
||||
reg := happydns.UserRegistration{
|
||||
Email: "test@example.com",
|
||||
Password: "123",
|
||||
Email: "max72@example.com",
|
||||
Password: pw72,
|
||||
}
|
||||
|
||||
_, err := service.CreateAuthUser(reg)
|
||||
if err == nil || err.Error() != "password must be at least 8 characters long" {
|
||||
t.Errorf("expected password constraint error, got: %v", err)
|
||||
if err != nil {
|
||||
t.Fatalf("expected 72-char password to be accepted, got %v", err)
|
||||
}
|
||||
|
||||
reg.Password = "Secur3$"
|
||||
_, err = service.CreateAuthUser(reg)
|
||||
if err == nil || err.Error() != "password must be at least 8 characters long" {
|
||||
t.Errorf("expected password constraint error, got: %v", err)
|
||||
// 73 characters should be rejected
|
||||
pw73 := pw72 + "x"
|
||||
reg = happydns.UserRegistration{
|
||||
Email: "max73@example.com",
|
||||
Password: pw73,
|
||||
}
|
||||
|
||||
reg.Password = "secure123"
|
||||
_, err = service.CreateAuthUser(reg)
|
||||
if err == nil || err.Error() != "Password must contain upper case letters." {
|
||||
t.Errorf("expected password constraint error, got: %v", err)
|
||||
}
|
||||
|
||||
reg.Password = "Secure123"
|
||||
_, err = service.CreateAuthUser(reg)
|
||||
if err == nil || err.Error() != "Password must be longer or contain symbols." {
|
||||
t.Errorf("expected password constraint error, got: %v", err)
|
||||
}
|
||||
requireValidationError(t, err)
|
||||
}
|
||||
|
||||
func TestCreateAuthUser_EmailAlreadyUsed(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
// Create a user first
|
||||
reg := happydns.UserRegistration{
|
||||
Email: "used@example.com",
|
||||
Password: "StrongPassword123!",
|
||||
|
|
@ -183,10 +221,14 @@ func TestCreateAuthUser_EmailAlreadyUsed(t *testing.T) {
|
|||
t.Fatalf("setup user creation failed: %v", err)
|
||||
}
|
||||
|
||||
// Try creating again with the same email
|
||||
_, err = service.CreateAuthUser(reg)
|
||||
if err == nil || err.Error() != "an account already exists with the given address. Try logging in." {
|
||||
t.Errorf("expected duplicate email error, got: %v", err)
|
||||
// Try creating again with the same email.
|
||||
// The implementation silently succeeds (returns nil, nil) to prevent user enumeration.
|
||||
user, err := service.CreateAuthUser(reg)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for duplicate email (anti-enumeration), got: %v", err)
|
||||
}
|
||||
if user != nil {
|
||||
t.Errorf("expected nil user for duplicate email, got non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +254,7 @@ func TestGetAuthUser(t *testing.T) {
|
|||
t.Fatalf("Expected non-nil user ID, got %s", user.Id)
|
||||
}
|
||||
|
||||
t.Run("GetAuthUser returns the correct user", func(t *testing.T) {
|
||||
t.Run("returns the correct user", func(t *testing.T) {
|
||||
got, err := service.GetAuthUser(user.Id)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
|
|
@ -222,7 +264,7 @@ func TestGetAuthUser(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("GetAuthUserByEmail returns the correct user", func(t *testing.T) {
|
||||
t.Run("by email returns the correct user", func(t *testing.T) {
|
||||
got, err := service.GetAuthUserByEmail("test@example.com")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
|
|
@ -232,14 +274,14 @@ func TestGetAuthUser(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("GetAuthUser returns error for unknown ID", func(t *testing.T) {
|
||||
t.Run("returns error for unknown ID", func(t *testing.T) {
|
||||
_, err := service.GetAuthUser([]byte("unknown-id"))
|
||||
if err == nil {
|
||||
t.Error("Expected error for unknown ID, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetAuthUserByEmail returns error for unknown email", func(t *testing.T) {
|
||||
t.Run("returns error for unknown email", func(t *testing.T) {
|
||||
_, err := service.GetAuthUserByEmail("unknown@example.com")
|
||||
if err == nil {
|
||||
t.Error("Expected error for unknown email, got nil")
|
||||
|
|
@ -277,6 +319,24 @@ func TestChangePassword(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChangePassword_WeakNewPassword(t *testing.T) {
|
||||
service, store := setupTestService()
|
||||
|
||||
user := &happydns.UserAuth{
|
||||
Email: "test@example.com",
|
||||
}
|
||||
user.DefinePassword("OldPassword123!")
|
||||
store.CreateAuthUser(user)
|
||||
|
||||
err := service.ChangePassword(user, "short")
|
||||
requireValidationError(t, err)
|
||||
|
||||
// Verify old password still works (change was not applied)
|
||||
if !user.CheckPassword("OldPassword123!") {
|
||||
t.Error("expected old password to still be valid after failed change")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPassword(t *testing.T) {
|
||||
service, store := setupTestService()
|
||||
|
||||
|
|
@ -290,7 +350,7 @@ func TestCheckPassword(t *testing.T) {
|
|||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
t.Run("CheckPassword with correct current password", func(t *testing.T) {
|
||||
t.Run("correct current password", func(t *testing.T) {
|
||||
form := happydns.ChangePasswordForm{
|
||||
Current: "OldPassword123!",
|
||||
Password: "NewPa$$w0rd",
|
||||
|
|
@ -302,7 +362,7 @@ func TestCheckPassword(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("CheckPassword with incorrect current password", func(t *testing.T) {
|
||||
t.Run("incorrect current password", func(t *testing.T) {
|
||||
form := happydns.ChangePasswordForm{
|
||||
Current: "WrongPassword123!",
|
||||
Password: "NewPa$$w0rd",
|
||||
|
|
@ -313,6 +373,16 @@ func TestCheckPassword(t *testing.T) {
|
|||
t.Error("Expected error for incorrect current password")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("correct current but weak new password", func(t *testing.T) {
|
||||
form := happydns.ChangePasswordForm{
|
||||
Current: "OldPassword123!",
|
||||
Password: "weak",
|
||||
PasswordConfirm: "weak",
|
||||
}
|
||||
err := service.CheckPassword(user, form)
|
||||
requireValidationError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckNewPassword(t *testing.T) {
|
||||
|
|
@ -322,7 +392,7 @@ func TestCheckNewPassword(t *testing.T) {
|
|||
Email: "test@example.com",
|
||||
}
|
||||
|
||||
t.Run("CheckNewPassword with matching passwords", func(t *testing.T) {
|
||||
t.Run("matching passwords", func(t *testing.T) {
|
||||
form := happydns.ChangePasswordForm{
|
||||
Password: "NewPa$$w0rd",
|
||||
PasswordConfirm: "NewPa$$w0rd",
|
||||
|
|
@ -333,14 +403,23 @@ func TestCheckNewPassword(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("CheckNewPassword with non-matching passwords", func(t *testing.T) {
|
||||
t.Run("non-matching passwords", func(t *testing.T) {
|
||||
form := happydns.ChangePasswordForm{
|
||||
Password: "NewPa$$w0rd",
|
||||
PasswordConfirm: "DifferentPassword123!",
|
||||
}
|
||||
err := service.CheckNewPassword(user, form)
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-matching passwords")
|
||||
requireValidationError(t, err)
|
||||
})
|
||||
|
||||
t.Run("empty confirmation is accepted", func(t *testing.T) {
|
||||
form := happydns.ChangePasswordForm{
|
||||
Password: "NewPa$$w0rd",
|
||||
PasswordConfirm: "",
|
||||
}
|
||||
err := service.CheckNewPassword(user, form)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected empty confirmation to be accepted, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -358,7 +437,7 @@ func TestDeleteAuthUser(t *testing.T) {
|
|||
return nil
|
||||
},
|
||||
}
|
||||
service := authuser.NewAuthUserUsecases(cfg, nil, store, mockCloseSessions)
|
||||
service := authuser.NewAuthUserUsecases(cfg, &NoopMailer{}, store, mockCloseSessions)
|
||||
|
||||
user := &happydns.UserAuth{
|
||||
Email: "test@example.com",
|
||||
|
|
@ -370,24 +449,24 @@ func TestDeleteAuthUser(t *testing.T) {
|
|||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
t.Run("DeleteAuthUser with invalid password", func(t *testing.T) {
|
||||
t.Run("invalid password", func(t *testing.T) {
|
||||
err := service.DeleteAuthUser(user, "WrongPassword")
|
||||
if err == nil || err.Error() != "invalid current password" {
|
||||
t.Errorf("Expected error 'invalid current password', got %v", err)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid password")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteAuthUser with error in closing sessions", func(t *testing.T) {
|
||||
t.Run("error in closing sessions", func(t *testing.T) {
|
||||
mockCloseSessions.CloseAllFunc = func(user happydns.UserInfo) error {
|
||||
return fmt.Errorf("error closing sessions")
|
||||
}
|
||||
err := service.DeleteAuthUser(user, "TestPassword123!")
|
||||
if err == nil || err.Error() != "unable to delete user sessions: error closing sessions" {
|
||||
t.Errorf("Expected error 'unable to delete user sessions: error closing sessions', got %v", err)
|
||||
if err == nil {
|
||||
t.Error("expected error when session close fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteAuthUser successful deletion", func(t *testing.T) {
|
||||
t.Run("successful deletion", func(t *testing.T) {
|
||||
mockCloseSessions.CloseAllFunc = func(user happydns.UserInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -395,5 +474,500 @@ func TestDeleteAuthUser(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Verify user is gone
|
||||
_, err = store.GetAuthUser(user.Id)
|
||||
if err == nil {
|
||||
t.Error("expected error when fetching deleted user")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ========== GenRegistrationHash Tests ==========
|
||||
|
||||
func TestGenRegistrationHash_Deterministic(t *testing.T) {
|
||||
createdAt := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
|
||||
key := []byte("test-recovery-key-for-registration-hash-0123456")
|
||||
|
||||
hash1 := authuser.GenRegistrationHash(createdAt, key, false)
|
||||
hash2 := authuser.GenRegistrationHash(createdAt, key, false)
|
||||
|
||||
if hash1 == "" {
|
||||
t.Fatal("expected non-empty hash")
|
||||
}
|
||||
if hash1 != hash2 {
|
||||
t.Error("expected identical hashes for same input and time period")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenRegistrationHash_EmptyKey(t *testing.T) {
|
||||
createdAt := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
|
||||
|
||||
hash := authuser.GenRegistrationHash(createdAt, nil, false)
|
||||
if hash != "" {
|
||||
t.Errorf("expected empty hash for nil key, got %q", hash)
|
||||
}
|
||||
|
||||
hash = authuser.GenRegistrationHash(createdAt, []byte{}, false)
|
||||
if hash != "" {
|
||||
t.Errorf("expected empty hash for empty key, got %q", hash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenRegistrationHash_DifferentPeriods(t *testing.T) {
|
||||
createdAt := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
|
||||
key := []byte("test-recovery-key-for-registration-hash-0123456")
|
||||
|
||||
current := authuser.GenRegistrationHash(createdAt, key, false)
|
||||
previous := authuser.GenRegistrationHash(createdAt, key, true)
|
||||
|
||||
if current == "" || previous == "" {
|
||||
t.Error("expected non-empty hashes for both periods")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenRegistrationHash_DifferentCreatedAt(t *testing.T) {
|
||||
key := []byte("shared-key-for-different-createdat-test-1234567")
|
||||
createdAt1 := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
|
||||
createdAt2 := time.Date(2025, 6, 20, 14, 0, 0, 0, time.UTC)
|
||||
|
||||
hash1 := authuser.GenRegistrationHash(createdAt1, key, false)
|
||||
hash2 := authuser.GenRegistrationHash(createdAt2, key, false)
|
||||
|
||||
if hash1 == hash2 {
|
||||
t.Error("expected different hashes for different CreatedAt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenRegistrationHash_DifferentKeys(t *testing.T) {
|
||||
createdAt := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
|
||||
key1 := []byte("key-one-for-registration-hash-different-keys-test")
|
||||
key2 := []byte("key-two-for-registration-hash-different-keys-test")
|
||||
|
||||
hash1 := authuser.GenRegistrationHash(createdAt, key1, false)
|
||||
hash2 := authuser.GenRegistrationHash(createdAt, key2, false)
|
||||
|
||||
if hash1 == hash2 {
|
||||
t.Error("expected different hashes for different keys")
|
||||
}
|
||||
}
|
||||
|
||||
// ========== GenAccountRecoveryHash Tests ==========
|
||||
|
||||
func TestGenAccountRecoveryHash_Deterministic(t *testing.T) {
|
||||
key := []byte("some-secret-recovery-key-for-testing-1234567890")
|
||||
|
||||
hash1 := authuser.GenAccountRecoveryHash(key, false)
|
||||
hash2 := authuser.GenAccountRecoveryHash(key, false)
|
||||
|
||||
if hash1 == "" {
|
||||
t.Fatal("expected non-empty hash")
|
||||
}
|
||||
if hash1 != hash2 {
|
||||
t.Error("expected identical hashes for same key and time period")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenAccountRecoveryHash_EmptyKey(t *testing.T) {
|
||||
hash := authuser.GenAccountRecoveryHash(nil, false)
|
||||
if hash != "" {
|
||||
t.Errorf("expected empty hash for nil key, got %q", hash)
|
||||
}
|
||||
|
||||
hash = authuser.GenAccountRecoveryHash([]byte{}, false)
|
||||
if hash != "" {
|
||||
t.Errorf("expected empty hash for empty key, got %q", hash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenAccountRecoveryHash_DifferentKeys(t *testing.T) {
|
||||
key1 := []byte("key-one-for-testing-recovery-hash-generation")
|
||||
key2 := []byte("key-two-for-testing-recovery-hash-generation")
|
||||
|
||||
hash1 := authuser.GenAccountRecoveryHash(key1, false)
|
||||
hash2 := authuser.GenAccountRecoveryHash(key2, false)
|
||||
|
||||
if hash1 == hash2 {
|
||||
t.Error("expected different hashes for different keys")
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CanRecoverAccount Tests ==========
|
||||
|
||||
func TestCanRecoverAccount_ValidKey(t *testing.T) {
|
||||
key := []byte("recovery-key-for-can-recover-test-1234567890ab")
|
||||
user := &happydns.UserAuth{
|
||||
Email: "test@example.com",
|
||||
PasswordRecoveryKey: key,
|
||||
}
|
||||
|
||||
validHash := authuser.GenAccountRecoveryHash(key, false)
|
||||
err := authuser.CanRecoverAccount(user, validHash)
|
||||
if err != nil {
|
||||
t.Fatalf("expected valid key to be accepted, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanRecoverAccount_PreviousPeriodKey(t *testing.T) {
|
||||
key := []byte("recovery-key-for-previous-period-test-12345678")
|
||||
user := &happydns.UserAuth{
|
||||
Email: "test@example.com",
|
||||
PasswordRecoveryKey: key,
|
||||
}
|
||||
|
||||
previousHash := authuser.GenAccountRecoveryHash(key, true)
|
||||
err := authuser.CanRecoverAccount(user, previousHash)
|
||||
if err != nil {
|
||||
t.Fatalf("expected previous-period key to be accepted, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanRecoverAccount_InvalidKey(t *testing.T) {
|
||||
key := []byte("recovery-key-for-invalid-key-test-1234567890ab")
|
||||
user := &happydns.UserAuth{
|
||||
Email: "test@example.com",
|
||||
PasswordRecoveryKey: key,
|
||||
}
|
||||
|
||||
err := authuser.CanRecoverAccount(user, "totally-invalid-key")
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid recovery key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanRecoverAccount_NilRecoveryKey(t *testing.T) {
|
||||
user := &happydns.UserAuth{
|
||||
Email: "test@example.com",
|
||||
PasswordRecoveryKey: nil,
|
||||
}
|
||||
|
||||
err := authuser.CanRecoverAccount(user, "any-key")
|
||||
if err == nil {
|
||||
t.Error("expected error when user has no recovery key")
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Email Validation Flow Tests ==========
|
||||
|
||||
func TestEmailValidation_GenerateLink(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "validate@example.com",
|
||||
Password: "StrongPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
link, err := service.GenerateValidationLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if link == "" {
|
||||
t.Fatal("expected non-empty validation link")
|
||||
}
|
||||
if !strings.Contains(link, "/email-validation") {
|
||||
t.Errorf("expected link to contain /email-validation, got %s", link)
|
||||
}
|
||||
if !strings.Contains(link, "u=") || !strings.Contains(link, "k=") {
|
||||
t.Errorf("expected link to contain u= and k= parameters, got %s", link)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailValidation_ValidateSuccess(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "validate@example.com",
|
||||
Password: "StrongPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
if user.EmailVerification != nil {
|
||||
t.Fatal("expected EmailVerification to be nil before validation")
|
||||
}
|
||||
|
||||
// Ensure recovery key exists (GenerateValidationLink generates it as side effect)
|
||||
_, err = service.GenerateValidationLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate validation link: %v", err)
|
||||
}
|
||||
|
||||
key := authuser.GenRegistrationHash(user.CreatedAt, user.PasswordRecoveryKey, false)
|
||||
err = service.ValidateEmail(user, happydns.AddressValidationForm{Key: key})
|
||||
if err != nil {
|
||||
t.Fatalf("expected validation to succeed, got %v", err)
|
||||
}
|
||||
|
||||
if user.EmailVerification == nil {
|
||||
t.Error("expected EmailVerification to be set after validation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailValidation_ValidateWithPreviousPeriodKey(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "validate-prev@example.com",
|
||||
Password: "StrongPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
_, err = service.GenerateValidationLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate validation link: %v", err)
|
||||
}
|
||||
|
||||
key := authuser.GenRegistrationHash(user.CreatedAt, user.PasswordRecoveryKey, true)
|
||||
err = service.ValidateEmail(user, happydns.AddressValidationForm{Key: key})
|
||||
if err != nil {
|
||||
t.Fatalf("expected previous-period key to be accepted, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailValidation_ValidateInvalidKey(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "validate-bad@example.com",
|
||||
Password: "StrongPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
err = service.ValidateEmail(user, happydns.AddressValidationForm{Key: "invalid-key"})
|
||||
requireValidationError(t, err)
|
||||
|
||||
if user.EmailVerification != nil {
|
||||
t.Error("expected EmailVerification to remain nil after failed validation")
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Recovery Flow Tests ==========
|
||||
|
||||
func TestRecovery_GenerateLink(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "recover@example.com",
|
||||
Password: "StrongPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
link, err := service.GenerateRecoveryLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if link == "" {
|
||||
t.Fatal("expected non-empty recovery link")
|
||||
}
|
||||
if !strings.Contains(link, "/forgotten-password") {
|
||||
t.Errorf("expected link to contain /forgotten-password, got %s", link)
|
||||
}
|
||||
if !strings.Contains(link, "u=") || !strings.Contains(link, "k=") {
|
||||
t.Errorf("expected link to contain u= and k= parameters, got %s", link)
|
||||
}
|
||||
|
||||
if user.PasswordRecoveryKey == nil {
|
||||
t.Error("expected PasswordRecoveryKey to be set after generating link")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecovery_GenerateLinkIdempotent(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "recover-idem@example.com",
|
||||
Password: "StrongPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
link1, err := service.GenerateRecoveryLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error on first call, got %v", err)
|
||||
}
|
||||
|
||||
link2, err := service.GenerateRecoveryLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error on second call, got %v", err)
|
||||
}
|
||||
|
||||
if link1 != link2 {
|
||||
t.Error("expected same link for repeated calls (key already exists)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecovery_ResetPasswordSuccess(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "reset@example.com",
|
||||
Password: "OldPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
_, err = service.GenerateRecoveryLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate recovery link: %v", err)
|
||||
}
|
||||
|
||||
key := authuser.GenAccountRecoveryHash(user.PasswordRecoveryKey, false)
|
||||
newPassword := "NewPa$$w0rd99"
|
||||
|
||||
err = service.ResetPassword(user, happydns.AccountRecoveryForm{
|
||||
Key: key,
|
||||
Password: newPassword,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected password reset to succeed, got %v", err)
|
||||
}
|
||||
|
||||
if !user.CheckPassword(newPassword) {
|
||||
t.Error("expected new password to work after reset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecovery_ResetPasswordInvalidKey(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "reset-bad@example.com",
|
||||
Password: "OldPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
_, err = service.GenerateRecoveryLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate recovery link: %v", err)
|
||||
}
|
||||
|
||||
err = service.ResetPassword(user, happydns.AccountRecoveryForm{
|
||||
Key: "invalid-key",
|
||||
Password: "NewPa$$w0rd99",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid recovery key")
|
||||
}
|
||||
|
||||
if !user.CheckPassword("OldPassword123!") {
|
||||
t.Error("expected old password to still work after failed reset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecovery_ResetPasswordWeakNewPassword(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "reset-weak@example.com",
|
||||
Password: "OldPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
_, err = service.GenerateRecoveryLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate recovery link: %v", err)
|
||||
}
|
||||
|
||||
key := authuser.GenAccountRecoveryHash(user.PasswordRecoveryKey, false)
|
||||
|
||||
err = service.ResetPassword(user, happydns.AccountRecoveryForm{
|
||||
Key: key,
|
||||
Password: "weak",
|
||||
})
|
||||
requireValidationError(t, err)
|
||||
}
|
||||
|
||||
func TestRecovery_ResetPasswordInvalidatesKey(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "reset-invalidate@example.com",
|
||||
Password: "OldPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
_, err = service.GenerateRecoveryLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate recovery link: %v", err)
|
||||
}
|
||||
|
||||
key := authuser.GenAccountRecoveryHash(user.PasswordRecoveryKey, false)
|
||||
|
||||
err = service.ResetPassword(user, happydns.AccountRecoveryForm{
|
||||
Key: key,
|
||||
Password: "NewPa$$w0rd99",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected first reset to succeed, got %v", err)
|
||||
}
|
||||
|
||||
// DefinePassword clears PasswordRecoveryKey, so the same key should no longer work
|
||||
if user.PasswordRecoveryKey != nil {
|
||||
t.Error("expected PasswordRecoveryKey to be nil after password reset")
|
||||
}
|
||||
|
||||
err = authuser.CanRecoverAccount(user, key)
|
||||
if err == nil {
|
||||
t.Error("expected recovery key to be invalidated after successful reset")
|
||||
}
|
||||
}
|
||||
|
||||
// ========== SendRecoveryLink Tests ==========
|
||||
|
||||
func TestSendRecoveryLink(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "send-recover@example.com",
|
||||
Password: "StrongPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
err = service.SendRecoveryLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if user.PasswordRecoveryKey == nil {
|
||||
t.Error("expected PasswordRecoveryKey to be set after sending recovery link")
|
||||
}
|
||||
}
|
||||
|
||||
// ========== SendValidationLink Tests ==========
|
||||
|
||||
func TestSendValidationLink(t *testing.T) {
|
||||
service, _ := setupTestService()
|
||||
|
||||
user, err := service.CreateAuthUser(happydns.UserRegistration{
|
||||
Email: "send-validate@example.com",
|
||||
Password: "StrongPassword123!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
err = service.SendValidationLink(user)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
internal/usecase/authuser/doc.go
Normal file
27
internal/usecase/authuser/doc.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package authuser groups all use cases related to authenticated user
|
||||
// management: registration, password changes, email validation, account
|
||||
// recovery, and deletion. The Service type is the main entry point; it
|
||||
// composes EmailValidationUsecase and RecoverAccountUsecase for their
|
||||
// respective sub-workflows.
|
||||
package authuser
|
||||
|
|
@ -27,9 +27,7 @@ import (
|
|||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/mail"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
|
|
@ -111,11 +109,6 @@ func (uc *RecoverAccountUsecase) SendLink(user *happydns.UserAuth) error {
|
|||
|
||||
toName := helpers.GenUsername(user.Email)
|
||||
|
||||
if uc.mailer == nil || reflect.ValueOf(uc.mailer).IsNil() {
|
||||
log.Printf("No mailer configured. Recovery link for %s: %s", user.Email, link)
|
||||
return nil
|
||||
}
|
||||
|
||||
return uc.mailer.SendMail(
|
||||
&mail.Address{Name: toName, Address: user.Email},
|
||||
"Recover your happyDomain account",
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ package authuser
|
|||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
|
|
@ -38,18 +38,24 @@ import (
|
|||
const RegistrationHashValidity = 24 * time.Hour
|
||||
|
||||
// GenRegistrationHash generates the validation hash for the current or previous period.
|
||||
// The hash computation is based on some already filled fields in the structure.
|
||||
func GenRegistrationHash(u *happydns.UserAuth, previous bool) string {
|
||||
// The hash uses both CreatedAt and PasswordRecoveryKey as HMAC key material,
|
||||
// ensuring the hash cannot be forged without knowledge of the secret recovery key.
|
||||
func GenRegistrationHash(createdAt time.Time, recoveryKey []byte, previous bool) string {
|
||||
if len(recoveryKey) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
date := time.Now()
|
||||
if previous {
|
||||
date = date.Add(RegistrationHashValidity * -1)
|
||||
}
|
||||
date = date.Truncate(RegistrationHashValidity)
|
||||
|
||||
h := hmac.New(
|
||||
sha512.New,
|
||||
[]byte(u.CreatedAt.Format(time.RFC3339Nano)),
|
||||
)
|
||||
// Combine CreatedAt and PasswordRecoveryKey as key material.
|
||||
// This differentiates from GenAccountRecoveryHash which uses only recoveryKey.
|
||||
keyMaterial := append([]byte(createdAt.Format(time.RFC3339Nano)), recoveryKey...)
|
||||
|
||||
h := hmac.New(sha512.New, keyMaterial)
|
||||
h.Write(date.AppendFormat([]byte{}, time.RFC3339))
|
||||
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
|
@ -70,16 +76,41 @@ func NewEmailValidationUsecase(store AuthUserStorage, mailer happydns.Mailer, co
|
|||
}
|
||||
}
|
||||
|
||||
// GenerateLink returns the absolute URL corresponding to the recovery
|
||||
// URL of the given account.
|
||||
func (uc *EmailValidationUsecase) GenerateLink(user *happydns.UserAuth) string {
|
||||
return uc.config.GetBaseURL() + fmt.Sprintf("/email-validation?u=%s&k=%s", base64.RawURLEncoding.EncodeToString(user.Id), GenRegistrationHash(user, false))
|
||||
// GenerateLink returns the absolute URL corresponding to the email
|
||||
// validation URL of the given account. It generates a PasswordRecoveryKey
|
||||
// if one does not already exist.
|
||||
func (uc *EmailValidationUsecase) GenerateLink(user *happydns.UserAuth) (string, error) {
|
||||
if err := uc.ensureRecoveryKey(user); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash := GenRegistrationHash(user.CreatedAt, user.PasswordRecoveryKey, false)
|
||||
return uc.config.GetBaseURL() + fmt.Sprintf("/email-validation?u=%s&k=%s", base64.RawURLEncoding.EncodeToString(user.Id), hash), nil
|
||||
}
|
||||
|
||||
// ensureRecoveryKey generates and persists a PasswordRecoveryKey if the user doesn't have one.
|
||||
func (uc *EmailValidationUsecase) ensureRecoveryKey(user *happydns.UserAuth) error {
|
||||
if user.PasswordRecoveryKey != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
user.PasswordRecoveryKey = make([]byte, 64)
|
||||
if _, err := rand.Read(user.PasswordRecoveryKey); err != nil {
|
||||
return fmt.Errorf("unable to generate recovery key: %w", err)
|
||||
}
|
||||
|
||||
if err := uc.store.UpdateAuthUser(user); err != nil {
|
||||
return fmt.Errorf("unable to save recovery key: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendLink sends an email validation link to the user's email.
|
||||
func (uc *EmailValidationUsecase) SendLink(user *happydns.UserAuth) error {
|
||||
if uc.mailer == nil || reflect.ValueOf(uc.mailer).IsNil() {
|
||||
return fmt.Errorf("no mailer configured")
|
||||
link, err := uc.GenerateLink(user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate validation link: %w", err)
|
||||
}
|
||||
|
||||
toName := helpers.GenUsername(user.Email)
|
||||
|
|
@ -98,13 +129,15 @@ management platform!
|
|||
In order to validate your account, please follow this link now:
|
||||
|
||||
[Validate my account](%s)
|
||||
`, toName, uc.GenerateLink(user)),
|
||||
`, toName, link),
|
||||
)
|
||||
}
|
||||
|
||||
// Validate tries to validate the email address by comparing the given key to the expected one.
|
||||
func (uc *EmailValidationUsecase) Validate(user *happydns.UserAuth, form happydns.AddressValidationForm) error {
|
||||
if form.Key != GenRegistrationHash(user, false) && form.Key != GenRegistrationHash(user, true) {
|
||||
currentHash := GenRegistrationHash(user.CreatedAt, user.PasswordRecoveryKey, false)
|
||||
previousHash := GenRegistrationHash(user.CreatedAt, user.PasswordRecoveryKey, true)
|
||||
if currentHash == "" || (form.Key != currentHash && form.Key != previousHash) {
|
||||
return happydns.ValidationError{Msg: fmt.Sprintf("bad email validation key: the validation address link you follow is invalid or has expired (it is valid during %d hours)", RegistrationHashValidity/time.Hour)}
|
||||
}
|
||||
|
||||
|
|
|
|||
28
internal/usecase/doc.go
Normal file
28
internal/usecase/doc.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package usecase implements the top-level application use cases for
|
||||
// happyDomain. Each file wires together lower-level domain services to fulfil
|
||||
// a single business capability: user authentication, DNS resolution, provider
|
||||
// settings wizard, and database tidy-up. These usecases are consumed directly
|
||||
// by the HTTP/API layer and delegate persistence to the storage interfaces
|
||||
// defined in the sub-packages.
|
||||
package usecase
|
||||
27
internal/usecase/domain/doc.go
Normal file
27
internal/usecase/domain/doc.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package domain implements the use cases for managing DNS domains in
|
||||
// happyDomain. It covers the full lifecycle of a domain: creation (including
|
||||
// existence verification against the provider), retrieval, update, and
|
||||
// deletion. It also exposes helpers for enriching a domain with its zone
|
||||
// metadata history.
|
||||
package domain
|
||||
|
|
@ -122,7 +122,7 @@ func createTestProvider(t *testing.T, store storage.Storage, user *happydns.User
|
|||
|
||||
func setupTestService(store storage.Storage) (*domain.Service, *mockDomainLogAppender) {
|
||||
// Create the provider service
|
||||
providerService := providerUC.NewService(store)
|
||||
providerService := providerUC.NewService(store, nil)
|
||||
|
||||
// Create the zone usecase
|
||||
getZone := zoneUC.NewGetZoneUsecase(store)
|
||||
|
|
|
|||
28
internal/usecase/domain_log/doc.go
Normal file
28
internal/usecase/domain_log/doc.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package domainlog implements the use cases for domain audit-log management.
|
||||
// Each significant action performed on a domain (import, publish, error) is
|
||||
// recorded as a DomainLog entry. The Service type exposes CRUD operations on
|
||||
// those entries, returning them sorted by date (newest first). The
|
||||
// DomainLogAppender interface is intentionally narrow so other packages can
|
||||
// append log entries without depending on the full Service.
|
||||
package domainlog
|
||||
37
internal/usecase/domain_log/noop.go
Normal file
37
internal/usecase/domain_log/noop.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package domainlog
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// NoopDomainLogAppender is a fallback implementation of DomainLogAppender
|
||||
// that prints log entries to stdout instead of persisting them.
|
||||
type NoopDomainLogAppender struct{}
|
||||
|
||||
func (NoopDomainLogAppender) AppendDomainLog(domain *happydns.Domain, entry *happydns.DomainLog) error {
|
||||
log.Printf("domain=%s %s\n", domain.DomainName, entry.Content)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -25,16 +25,21 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// formUsecase implements happydns.FormUsecase, providing form-related helpers
|
||||
// such as base URL generation used when building dynamic forms.
|
||||
type formUsecase struct {
|
||||
config *happydns.Options
|
||||
}
|
||||
|
||||
// NewFormUsecase returns a FormUsecase backed by the given application options.
|
||||
func NewFormUsecase(cfg *happydns.Options) happydns.FormUsecase {
|
||||
return &formUsecase{
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBaseURL returns the application's base URL, used when constructing
|
||||
// absolute links inside forms (e.g. OAuth redirect URIs).
|
||||
func (fu *formUsecase) GetBaseURL() string {
|
||||
return fu.config.GetBaseURL()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,14 @@ type CollectStorage interface {
|
|||
}
|
||||
|
||||
// Collect gathers anonymous usage statistics about the running instance.
|
||||
func Collect(cfg *happydns.Options, store CollectStorage, instanceID string, version happydns.VersionResponse, buildSettings map[string]string, goVersion string) (*happydns.Insights, error) {
|
||||
func Collect(
|
||||
cfg *happydns.Options,
|
||||
store CollectStorage,
|
||||
instanceID string,
|
||||
version happydns.VersionResponse,
|
||||
buildSettings map[string]string,
|
||||
goVersion string,
|
||||
) (*happydns.Insights, error) {
|
||||
data := happydns.Insights{
|
||||
InsightsID: instanceID,
|
||||
Version: version,
|
||||
|
|
|
|||
27
internal/usecase/insight/doc.go
Normal file
27
internal/usecase/insight/doc.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package insight collects anonymous usage statistics about a running
|
||||
// happyDomain instance. The Collect function aggregates counters from the
|
||||
// storage layer (number of users, providers, domains, zones) together with
|
||||
// build metadata and runtime configuration flags into a single Insights
|
||||
// value that can be reported to the telemetry endpoint.
|
||||
package insight
|
||||
|
|
@ -19,9 +19,16 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package orchestrator wires together lower-level use-cases to implement the
|
||||
// multi-step workflows that span provider access, zone storage, and domain
|
||||
// history management. It sits between the HTTP/API layer and the individual
|
||||
// domain/zone use-cases, coordinating the sequence of operations required to
|
||||
// import, diff, and publish DNS zones.
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
domainlogUC "git.happydns.org/happyDomain/internal/usecase/domain_log"
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
|
|
@ -39,20 +46,32 @@ type ProviderGetter interface {
|
|||
|
||||
// ZoneRetriever is an interface for retrieving zones from providers.
|
||||
type ZoneRetriever interface {
|
||||
RetrieveZone(provider *happydns.Provider, name string) ([]happydns.Record, error)
|
||||
RetrieveZone(ctx context.Context, provider *happydns.Provider, name string) ([]happydns.Record, error)
|
||||
}
|
||||
|
||||
// ZoneCorrector is an interface for getting zone corrections.
|
||||
type ZoneCorrector interface {
|
||||
ListZoneCorrections(provider *happydns.Provider, domain *happydns.Domain, records []happydns.Record) ([]*happydns.Correction, int, error)
|
||||
ListZoneCorrections(ctx context.Context, provider *happydns.Provider, domain *happydns.Domain, records []happydns.Record) ([]*happydns.Correction, int, error)
|
||||
}
|
||||
|
||||
// Orchestrator aggregates the use-cases that together implement the DNS zone
|
||||
// lifecycle: importing zones from a provider, listing required corrections, and
|
||||
// applying those corrections back to the provider.
|
||||
type Orchestrator struct {
|
||||
RemoteZoneImporter *RemoteZoneImporterUsecase
|
||||
// RemoteZoneImporter fetches a live zone from the provider and stores it.
|
||||
RemoteZoneImporter *RemoteZoneImporterUsecase
|
||||
// ZoneCorrectionApplier lists and applies the corrections needed to bring
|
||||
// the provider in sync with the desired zone state.
|
||||
ZoneCorrectionApplier *ZoneCorrectionApplierUsecase
|
||||
ZoneImporter *ZoneImporterUsecase
|
||||
// ZoneImporter converts a flat list of DNS records into a happyDomain zone
|
||||
// and persists it in the domain history.
|
||||
ZoneImporter *ZoneImporterUsecase
|
||||
}
|
||||
|
||||
// NewOrchestrator constructs an Orchestrator by wiring up all required
|
||||
// dependencies. It builds the shared ZoneImporterUsecase and
|
||||
// ZoneCorrectionListerUsecase internally so callers do not need to manage
|
||||
// those intermediate objects.
|
||||
func NewOrchestrator(
|
||||
appendDomainLog domainlogUC.DomainLogAppender,
|
||||
domainUpdater DomainUpdater,
|
||||
|
|
@ -60,13 +79,15 @@ func NewOrchestrator(
|
|||
listRecords *zoneUC.ListRecordsUsecase,
|
||||
zoneCorrectorService ZoneCorrector,
|
||||
zoneCreator *zoneUC.CreateZoneUsecase,
|
||||
zoneGetter *zoneUC.GetZoneUsecase,
|
||||
zoneRetrieverService ZoneRetriever,
|
||||
zoneUpdater *zoneUC.UpdateZoneUsecase,
|
||||
) *Orchestrator {
|
||||
zoneImporter := NewZoneImporterUsecase(domainUpdater, zoneCreator)
|
||||
zoneImporter := NewZoneImporterUsecase(domainUpdater, zoneCreator, zoneGetter)
|
||||
zoneCorrectionLister := NewZoneCorrectionListerUsecase(providerService, listRecords, zoneCorrectorService, zoneRetrieverService)
|
||||
return &Orchestrator{
|
||||
RemoteZoneImporter: NewRemoteZoneImporterUsecase(appendDomainLog, providerService, zoneImporter, zoneRetrieverService),
|
||||
ZoneCorrectionApplier: NewZoneCorrectionApplierUsecase(appendDomainLog, domainUpdater, providerService, listRecords, zoneCorrectorService, zoneCreator, zoneUpdater),
|
||||
ZoneCorrectionApplier: NewZoneCorrectionApplierUsecase(appendDomainLog, domainUpdater, zoneCorrectionLister, zoneCreator, zoneGetter, zoneUpdater),
|
||||
ZoneImporter: zoneImporter,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,23 +22,30 @@
|
|||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
domainlogUC "git.happydns.org/happyDomain/internal/usecase/domain_log"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// RemoteZoneImporterUsecase fetches the live DNS records for a domain directly
|
||||
// from the provider and delegates to ZoneImporterUsecase to persist them. It
|
||||
// also appends a domain log entry on success.
|
||||
type RemoteZoneImporterUsecase struct {
|
||||
appendDomainLog domainlogUC.DomainLogAppender
|
||||
providerService ProviderGetter
|
||||
zoneImporter *ZoneImporterUsecase
|
||||
zoneImporter happydns.ZoneImporterUsecase
|
||||
zoneRetriever ZoneRetriever
|
||||
}
|
||||
|
||||
// NewRemoteZoneImporterUsecase creates a RemoteZoneImporterUsecase wired to
|
||||
// the given log appender, provider getter, zone importer, and zone retriever.
|
||||
func NewRemoteZoneImporterUsecase(
|
||||
appendDomainLog domainlogUC.DomainLogAppender,
|
||||
providerService ProviderGetter,
|
||||
zoneImporter *ZoneImporterUsecase,
|
||||
zoneImporter happydns.ZoneImporterUsecase,
|
||||
zoneRetriever ZoneRetriever,
|
||||
) *RemoteZoneImporterUsecase {
|
||||
return &RemoteZoneImporterUsecase{
|
||||
|
|
@ -49,25 +56,27 @@ func NewRemoteZoneImporterUsecase(
|
|||
}
|
||||
}
|
||||
|
||||
func (uc *RemoteZoneImporterUsecase) Import(user *happydns.User, domain *happydns.Domain) (*happydns.Zone, error) {
|
||||
// Import resolves the provider for the domain, retrieves its current records,
|
||||
// and imports them via ZoneImporterUsecase. A domain log entry is appended on
|
||||
// success. Returns the newly created zone or an error.
|
||||
func (uc *RemoteZoneImporterUsecase) Import(ctx context.Context, user *happydns.User, domain *happydns.Domain) (*happydns.Zone, error) {
|
||||
provider, err := uc.providerService.GetUserProvider(user, domain.ProviderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zone, err := uc.zoneRetriever.RetrieveZone(provider, domain.DomainName)
|
||||
zone, err := uc.zoneRetriever.RetrieveZone(ctx, provider, domain.DomainName)
|
||||
if err != nil {
|
||||
return nil, happydns.ValidationError{Msg: fmt.Sprintf("unable to retrieve the zone from server: %s", err.Error())}
|
||||
return nil, fmt.Errorf("unable to retrieve the zone from server: %w", err)
|
||||
}
|
||||
|
||||
// import
|
||||
myZone, err := uc.zoneImporter.Import(user, domain, zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if uc.appendDomainLog != nil {
|
||||
uc.appendDomainLog.AppendDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_INFO, fmt.Sprintf("Zone imported from provider API: %s", myZone.Id.String())))
|
||||
if err := uc.appendDomainLog.AppendDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_INFO, fmt.Sprintf("Zone imported from provider API: %s", myZone.Id.String()))); err != nil {
|
||||
log.Printf("unable to append domain log for %s: %s", domain.DomainName, err.Error())
|
||||
}
|
||||
|
||||
return myZone, nil
|
||||
|
|
@ -22,152 +22,219 @@
|
|||
package orchestrator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
adapter "git.happydns.org/happyDomain/internal/adapters"
|
||||
domainlogUC "git.happydns.org/happyDomain/internal/usecase/domain_log"
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
svcs "git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
// ZoneCorrectionApplierUsecase applies a user-selected subset of zone
|
||||
// corrections to the provider and, on success, creates a published snapshot
|
||||
// in the domain history. The WIP zone at ZoneHistory[0] is never modified.
|
||||
type ZoneCorrectionApplierUsecase struct {
|
||||
*ZoneCorrectionListerUsecase
|
||||
appendDomainLog domainlogUC.DomainLogAppender
|
||||
domainUpdater DomainUpdater
|
||||
providerService ProviderGetter
|
||||
listRecords *zoneUC.ListRecordsUsecase
|
||||
zoneCorrector ZoneCorrector
|
||||
zoneCreator *zoneUC.CreateZoneUsecase
|
||||
zoneGetter *zoneUC.GetZoneUsecase
|
||||
zoneUpdater *zoneUC.UpdateZoneUsecase
|
||||
clock func() time.Time
|
||||
}
|
||||
|
||||
// NewZoneCorrectionApplierUsecase creates a ZoneCorrectionApplierUsecase with
|
||||
// the given dependencies. The lister is embedded so that Apply can compute
|
||||
// the full correction diff in a single call.
|
||||
func NewZoneCorrectionApplierUsecase(
|
||||
appendDomainLog domainlogUC.DomainLogAppender,
|
||||
domainUpdater DomainUpdater,
|
||||
providerService ProviderGetter,
|
||||
listRecords *zoneUC.ListRecordsUsecase,
|
||||
zoneCorrector ZoneCorrector,
|
||||
lister *ZoneCorrectionListerUsecase,
|
||||
zoneCreator *zoneUC.CreateZoneUsecase,
|
||||
zoneGetter *zoneUC.GetZoneUsecase,
|
||||
zoneUpdater *zoneUC.UpdateZoneUsecase,
|
||||
) *ZoneCorrectionApplierUsecase {
|
||||
return &ZoneCorrectionApplierUsecase{
|
||||
appendDomainLog: appendDomainLog,
|
||||
domainUpdater: domainUpdater,
|
||||
providerService: providerService,
|
||||
listRecords: listRecords,
|
||||
zoneCorrector: zoneCorrector,
|
||||
zoneCreator: zoneCreator,
|
||||
zoneUpdater: zoneUpdater,
|
||||
ZoneCorrectionListerUsecase: lister,
|
||||
appendDomainLog: appendDomainLog,
|
||||
domainUpdater: domainUpdater,
|
||||
zoneCreator: zoneCreator,
|
||||
zoneGetter: zoneGetter,
|
||||
zoneUpdater: zoneUpdater,
|
||||
clock: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ZoneCorrectionApplierUsecase) Apply(user *happydns.User, domain *happydns.Domain, zone *happydns.Zone, form *happydns.ApplyZoneForm) (*happydns.Zone, error) {
|
||||
// computeExecutableCorrections computes the executable corrections for the
|
||||
// given selection. It performs the diff, builds the target record set, and asks
|
||||
// the provider what it would execute to reach that target state.
|
||||
func (uc *ZoneCorrectionApplierUsecase) computeExecutableCorrections(
|
||||
ctx context.Context,
|
||||
user *happydns.User,
|
||||
domain *happydns.Domain,
|
||||
zone *happydns.Zone,
|
||||
wantedCorrections []happydns.Identifier,
|
||||
) (execCorrections []*happydns.Correction, targetRecords []happydns.Record, nbDiffs int, err error) {
|
||||
// Step 1: Compute the diff and get provider/WIP records.
|
||||
corrections, providerRecords, _, nbDiffs, err := uc.listWithRecords(ctx, user, domain, zone)
|
||||
if err != nil {
|
||||
return nil, nil, nbDiffs, err
|
||||
}
|
||||
|
||||
// Step 2: Build target records from selected corrections.
|
||||
targetRecords = adapter.BuildTargetRecords(providerRecords, corrections, wantedCorrections)
|
||||
|
||||
// Step 3: Get executable corrections from the provider for the target state.
|
||||
provider, err := uc.providerService.GetUserProvider(user, domain.ProviderId)
|
||||
if err != nil {
|
||||
return nil, nil, nbDiffs, err
|
||||
}
|
||||
|
||||
execCorrections, nbDiffs, err = uc.zoneCorrector.ListZoneCorrections(ctx, provider, domain, targetRecords)
|
||||
if err != nil {
|
||||
return nil, nil, nbDiffs, fmt.Errorf("unable to compute executable corrections: %w", err)
|
||||
}
|
||||
|
||||
return execCorrections, targetRecords, nbDiffs, nil
|
||||
}
|
||||
|
||||
// Prepare computes the executable corrections for the given selection without
|
||||
// applying them. This lets the user see exactly what the provider will execute
|
||||
// before confirming.
|
||||
func (uc *ZoneCorrectionApplierUsecase) Prepare(
|
||||
ctx context.Context,
|
||||
user *happydns.User,
|
||||
domain *happydns.Domain,
|
||||
zone *happydns.Zone,
|
||||
form *happydns.PrepareZoneForm,
|
||||
) (*happydns.PrepareZoneResponse, error) {
|
||||
execCorrections, _, nbDiffs, err := uc.computeExecutableCorrections(ctx, user, domain, zone, form.WantedCorrections)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records, err := uc.listRecords.List(domain, zone)
|
||||
return &happydns.PrepareZoneResponse{
|
||||
Corrections: execCorrections,
|
||||
NbDiffs: nbDiffs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Apply executes the selected corrections against the provider and creates a
|
||||
// published snapshot zone inserted at ZoneHistory[1] (after the WIP zone at
|
||||
// position 0). The WIP zone is never modified.
|
||||
//
|
||||
// Flow:
|
||||
// 1. Compute the diff (corrections + provider/WIP records)
|
||||
// 2. Build the target record set from selected corrections
|
||||
// 3. Ask the provider to compute executable corrections for the target state
|
||||
// 4. Execute all returned corrections
|
||||
// 5. Create a published snapshot zone from the target records
|
||||
// 6. Insert the snapshot at ZoneHistory[1]
|
||||
// 7. Return the published snapshot zone
|
||||
func (uc *ZoneCorrectionApplierUsecase) Apply(
|
||||
ctx context.Context,
|
||||
user *happydns.User,
|
||||
domain *happydns.Domain,
|
||||
zone *happydns.Zone,
|
||||
form *happydns.ApplyZoneForm,
|
||||
) (*happydns.Zone, error) {
|
||||
executableCorrections, targetRecords, _, err := uc.computeExecutableCorrections(ctx, user, domain, zone, form.WantedCorrections)
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to retrieve records for zone: %w", err),
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nbcorrections := len(form.WantedCorrections)
|
||||
corrections, _, err := uc.zoneCorrector.ListZoneCorrections(provider, domain, records)
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to compute domain corrections: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
var errs error
|
||||
corrections:
|
||||
for i, cr := range corrections {
|
||||
for ic, wc := range form.WantedCorrections {
|
||||
if wc.Equals(cr.Id) {
|
||||
log.Printf("%s: apply correction: %s", domain.DomainName, cr.Msg)
|
||||
err := cr.F()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%s: unable to apply correction: %s", domain.DomainName, err.Error())
|
||||
uc.appendDomainLog.AppendDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_ERR, fmt.Sprintf("Failed record update (%s): %s", cr.Msg, err.Error())))
|
||||
errs = errors.Join(errs, fmt.Errorf("%s: %w", cr.Msg, err))
|
||||
// Stop the zone update if we didn't change it yet
|
||||
if i == 0 {
|
||||
break corrections
|
||||
}
|
||||
} else {
|
||||
form.WantedCorrections = append(form.WantedCorrections[:ic], form.WantedCorrections[ic+1:]...)
|
||||
}
|
||||
break
|
||||
// Step 4: Execute all corrections.
|
||||
appliedCount := 0
|
||||
for _, cr := range executableCorrections {
|
||||
log.Printf("%s: apply correction: %s", domain.DomainName, cr.Msg)
|
||||
if corrErr := cr.F(); corrErr != nil {
|
||||
log.Printf("%s: unable to apply correction: %s", domain.DomainName, corrErr.Error())
|
||||
if logErr := uc.appendDomainLog.AppendDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_ERR, fmt.Sprintf("Failed record update (%s): %s", cr.Msg, corrErr.Error()))); logErr != nil {
|
||||
log.Printf("unable to append domain log for %s: %s", domain.DomainName, logErr.Error())
|
||||
}
|
||||
if appliedCount == 0 {
|
||||
return nil, happydns.ValidationError{Msg: fmt.Sprintf("unable to apply correction: %s", corrErr.Error())}
|
||||
}
|
||||
if logErr := uc.appendDomainLog.AppendDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_ERR, fmt.Sprintf("Failed zone publishing (%s): %d of %d corrections applied, errors occurred.", zone.Id.String(), appliedCount, len(executableCorrections)))); logErr != nil {
|
||||
log.Printf("unable to append domain log for %s: %s", domain.DomainName, logErr.Error())
|
||||
}
|
||||
return nil, happydns.ValidationError{Msg: fmt.Sprintf("unable to update the zone (%d of %d corrections applied): %s", appliedCount, len(executableCorrections), corrErr.Error())}
|
||||
}
|
||||
appliedCount++
|
||||
}
|
||||
|
||||
if errs != nil {
|
||||
uc.appendDomainLog.AppendDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_ERR, fmt.Sprintf("Failed zone publishing (%s): %d corrections were not applied due to errors.", zone.Id.String(), nbcorrections)))
|
||||
return nil, happydns.ValidationError{Msg: fmt.Sprintf("unable to update the zone: %s", errs.Error())}
|
||||
} else if len(form.WantedCorrections) > 0 {
|
||||
uc.appendDomainLog.AppendDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_ERR, fmt.Sprintf("Failed zone publishing (%s): %d corrections were not applied.", zone.Id.String(), nbcorrections)))
|
||||
return nil, happydns.ValidationError{Msg: fmt.Sprintf("unable to perform the following changes: %s", form.WantedCorrections)}
|
||||
if logErr := uc.appendDomainLog.AppendDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_ACK, fmt.Sprintf("Zone published (%s), %d corrections applied with success", zone.Id.String(), appliedCount))); logErr != nil {
|
||||
log.Printf("unable to append domain log for %s: %s", domain.DomainName, logErr.Error())
|
||||
}
|
||||
|
||||
uc.appendDomainLog.AppendDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_ACK, fmt.Sprintf("Zone published (%s), %d corrections applied with success", zone.Id.String(), nbcorrections)))
|
||||
|
||||
// Create a new zone in history for futher updates
|
||||
newZone := zone.DerivateNew()
|
||||
err = uc.zoneCreator.Create(newZone)
|
||||
// Step 5: Create a published snapshot zone from target records.
|
||||
services, defaultTTL, err := svcs.AnalyzeZone(domain.DomainName, targetRecords)
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to CreateZone: %w", err),
|
||||
UserMessage: "Sorry, we are unable to create the zone now.",
|
||||
Err: fmt.Errorf("unable to analyze target zone: %w", err),
|
||||
UserMessage: "Sorry, we are unable to analyze the published zone.",
|
||||
}
|
||||
}
|
||||
|
||||
// Carry over metadata from WIP zone.
|
||||
if zone.Services != nil {
|
||||
zoneUC.ReassociateMetadata(zone.Services, services, domain.DomainName, defaultTTL)
|
||||
}
|
||||
|
||||
// Also carry over metadata from the previous published zone if available.
|
||||
if len(domain.ZoneHistory) > 1 {
|
||||
prevZone, prevErr := uc.zoneGetter.Get(domain.ZoneHistory[1])
|
||||
if prevErr != nil {
|
||||
log.Printf("ReassociateMetadata: unable to load previous zone %s: %s (metadata will not be transferred)", domain.ZoneHistory[1], prevErr)
|
||||
} else {
|
||||
zoneUC.ReassociateMetadata(prevZone.Services, services, domain.DomainName, defaultTTL)
|
||||
}
|
||||
}
|
||||
|
||||
now := uc.clock()
|
||||
snapshot := &happydns.Zone{
|
||||
ZoneMeta: happydns.ZoneMeta{
|
||||
IdAuthor: user.Id,
|
||||
DefaultTTL: defaultTTL,
|
||||
LastModified: now,
|
||||
CommitMsg: &form.CommitMsg,
|
||||
CommitDate: &now,
|
||||
Published: &now,
|
||||
ParentZone: &zone.ZoneMeta.Id,
|
||||
},
|
||||
Services: services,
|
||||
}
|
||||
|
||||
err = uc.zoneCreator.Create(snapshot)
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to CreateZone for published snapshot: %w", err),
|
||||
UserMessage: "Sorry, we are unable to create the published zone snapshot.",
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Insert snapshot at ZoneHistory[1] (after WIP at position 0).
|
||||
err = uc.domainUpdater.Update(domain.Id, user, func(domain *happydns.Domain) {
|
||||
domain.ZoneHistory = append(
|
||||
[]happydns.Identifier{newZone.Id}, domain.ZoneHistory...)
|
||||
if len(domain.ZoneHistory) == 0 {
|
||||
domain.ZoneHistory = []happydns.Identifier{snapshot.Id}
|
||||
} else {
|
||||
newHistory := make([]happydns.Identifier, 0, len(domain.ZoneHistory)+1)
|
||||
newHistory = append(newHistory, domain.ZoneHistory[0])
|
||||
newHistory = append(newHistory, snapshot.Id)
|
||||
newHistory = append(newHistory, domain.ZoneHistory[1:]...)
|
||||
domain.ZoneHistory = newHistory
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to UpdateDomain: %w", err),
|
||||
UserMessage: "Sorry, we are unable to create the zone now.",
|
||||
UserMessage: "Sorry, we are unable to update the domain history now.",
|
||||
}
|
||||
}
|
||||
|
||||
// Commit changes in previous zone
|
||||
err = uc.zoneUpdater.Update(zone.ZoneMeta.Id, func(zone *happydns.Zone) {
|
||||
now := time.Now()
|
||||
zone.ZoneMeta.IdAuthor = user.Id
|
||||
zone.CommitMsg = &form.CommitMsg
|
||||
zone.ZoneMeta.CommitDate = &now
|
||||
zone.ZoneMeta.Published = &now
|
||||
|
||||
zone.LastModified = time.Now()
|
||||
})
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to UpdateZone: %w", err),
|
||||
UserMessage: "Sorry, we are unable to create the zone now.",
|
||||
}
|
||||
}
|
||||
|
||||
return newZone, nil
|
||||
}
|
||||
|
||||
func (uc *ZoneCorrectionApplierUsecase) List(user *happydns.User, domain *happydns.Domain, zone *happydns.Zone) ([]*happydns.Correction, int, error) {
|
||||
provider, err := uc.providerService.GetUserProvider(user, domain.ProviderId)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
records, err := uc.listRecords.List(domain, zone)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return uc.zoneCorrector.ListZoneCorrections(provider, domain, records)
|
||||
return snapshot, nil
|
||||
}
|
||||
|
|
|
|||
102
internal/usecase/orchestrator/zone_correction_lister.go
Normal file
102
internal/usecase/orchestrator/zone_correction_lister.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
adapter "git.happydns.org/happyDomain/internal/adapters"
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// ZoneCorrectionListerUsecase computes the list of corrections needed to
|
||||
// synchronize a zone's desired state with the records currently published by
|
||||
// the provider. It fetches provider records, expands the WIP zone to records,
|
||||
// and computes a local diff without executable closures.
|
||||
type ZoneCorrectionListerUsecase struct {
|
||||
providerService ProviderGetter
|
||||
listRecords *zoneUC.ListRecordsUsecase
|
||||
zoneCorrector ZoneCorrector
|
||||
zoneRetriever ZoneRetriever
|
||||
}
|
||||
|
||||
// NewZoneCorrectionListerUsecase creates a ZoneCorrectionListerUsecase with
|
||||
// the given provider getter, record lister, zone corrector, and zone retriever.
|
||||
func NewZoneCorrectionListerUsecase(
|
||||
providerService ProviderGetter,
|
||||
listRecords *zoneUC.ListRecordsUsecase,
|
||||
zoneCorrector ZoneCorrector,
|
||||
zoneRetriever ZoneRetriever,
|
||||
) *ZoneCorrectionListerUsecase {
|
||||
return &ZoneCorrectionListerUsecase{
|
||||
providerService: providerService,
|
||||
listRecords: listRecords,
|
||||
zoneCorrector: zoneCorrector,
|
||||
zoneRetriever: zoneRetriever,
|
||||
}
|
||||
}
|
||||
|
||||
// listWithRecords is the internal implementation that returns the corrections
|
||||
// along with the provider and WIP records used to compute them.
|
||||
func (uc *ZoneCorrectionListerUsecase) listWithRecords(
|
||||
ctx context.Context,
|
||||
user *happydns.User,
|
||||
domain *happydns.Domain,
|
||||
zone *happydns.Zone,
|
||||
) ([]*happydns.Correction, []happydns.Record, []happydns.Record, int, error) {
|
||||
provider, err := uc.providerService.GetUserProvider(user, domain.ProviderId)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, err
|
||||
}
|
||||
|
||||
providerRecords, err := uc.zoneRetriever.RetrieveZone(ctx, provider, domain.DomainName)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, err
|
||||
}
|
||||
|
||||
wipRecords, err := uc.listRecords.List(domain, zone)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, err
|
||||
}
|
||||
|
||||
corrections, nbDiffs, err := adapter.DNSControlDiffByRecord(providerRecords, wipRecords, domain.DomainName)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nbDiffs, err
|
||||
}
|
||||
|
||||
return corrections, providerRecords, wipRecords, nbDiffs, nil
|
||||
}
|
||||
|
||||
// List returns the corrections required to bring the provider's live DNS
|
||||
// records in line with the given zone. It fetches the current provider
|
||||
// records, expands the zone into individual records, and computes the diff
|
||||
// locally. The second return value is the total number of corrections.
|
||||
func (uc *ZoneCorrectionListerUsecase) List(
|
||||
ctx context.Context,
|
||||
user *happydns.User,
|
||||
domain *happydns.Domain,
|
||||
zone *happydns.Zone,
|
||||
) ([]*happydns.Correction, int, error) {
|
||||
corrections, _, _, nbDiffs, err := uc.listWithRecords(ctx, user, domain, zone)
|
||||
return corrections, nbDiffs, err
|
||||
}
|
||||
188
internal/usecase/orchestrator/zone_correction_lister_test.go
Normal file
188
internal/usecase/orchestrator/zone_correction_lister_test.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package orchestrator_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/usecase/orchestrator"
|
||||
serviceUC "git.happydns.org/happyDomain/internal/usecase/service"
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// mockProviderGetter implements ProviderGetter for testing.
|
||||
type mockProviderGetter struct {
|
||||
provider *happydns.Provider
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockProviderGetter) GetUserProvider(_ *happydns.User, _ happydns.Identifier) (*happydns.Provider, error) {
|
||||
return m.provider, m.err
|
||||
}
|
||||
|
||||
// mockZoneCorrector implements ZoneCorrector for testing.
|
||||
type mockZoneCorrector struct {
|
||||
corrections []*happydns.Correction
|
||||
nbDiff int
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockZoneCorrector) ListZoneCorrections(_ context.Context, _ *happydns.Provider, _ *happydns.Domain, _ []happydns.Record) ([]*happydns.Correction, int, error) {
|
||||
return m.corrections, m.nbDiff, m.err
|
||||
}
|
||||
|
||||
// mockZoneRetriever implements ZoneRetriever for testing.
|
||||
type mockZoneRetriever struct {
|
||||
records []happydns.Record
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockZoneRetriever) RetrieveZone(_ context.Context, _ *happydns.Provider, _ string) ([]happydns.Record, error) {
|
||||
return m.records, m.err
|
||||
}
|
||||
|
||||
func newTestListRecordsUsecase() *zoneUC.ListRecordsUsecase {
|
||||
return zoneUC.NewListRecordsUsecase(serviceUC.NewListRecordsUsecase())
|
||||
}
|
||||
|
||||
func TestZoneCorrectionLister_List_Success(t *testing.T) {
|
||||
provider := &happydns.Provider{}
|
||||
|
||||
uc := orchestrator.NewZoneCorrectionListerUsecase(
|
||||
&mockProviderGetter{provider: provider},
|
||||
newTestListRecordsUsecase(),
|
||||
&mockZoneCorrector{},
|
||||
&mockZoneRetriever{records: nil},
|
||||
)
|
||||
|
||||
user := &happydns.User{Id: happydns.Identifier([]byte("test-user"))}
|
||||
domain := &happydns.Domain{
|
||||
Id: happydns.Identifier([]byte("test-domain")),
|
||||
ProviderId: happydns.Identifier([]byte("test-provider")),
|
||||
DomainName: "example.com.",
|
||||
}
|
||||
zone := &happydns.Zone{
|
||||
ZoneMeta: happydns.ZoneMeta{DefaultTTL: 3600},
|
||||
Services: map[happydns.Subdomain][]*happydns.Service{},
|
||||
}
|
||||
|
||||
got, nbDiff, err := uc.List(context.Background(), user, domain, zone)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if nbDiff != 0 {
|
||||
t.Errorf("expected nbDiff=0, got %d", nbDiff)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Errorf("expected 0 corrections, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneCorrectionLister_List_ProviderError(t *testing.T) {
|
||||
providerErr := errors.New("provider not found")
|
||||
|
||||
uc := orchestrator.NewZoneCorrectionListerUsecase(
|
||||
&mockProviderGetter{err: providerErr},
|
||||
newTestListRecordsUsecase(),
|
||||
&mockZoneCorrector{},
|
||||
&mockZoneRetriever{},
|
||||
)
|
||||
|
||||
user := &happydns.User{Id: happydns.Identifier([]byte("test-user"))}
|
||||
domain := &happydns.Domain{
|
||||
ProviderId: happydns.Identifier([]byte("missing-provider")),
|
||||
DomainName: "example.com.",
|
||||
}
|
||||
zone := &happydns.Zone{
|
||||
ZoneMeta: happydns.ZoneMeta{DefaultTTL: 3600},
|
||||
}
|
||||
|
||||
_, _, err := uc.List(context.Background(), user, domain, zone)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !errors.Is(err, providerErr) {
|
||||
t.Errorf("expected %v, got %v", providerErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneCorrectionLister_List_RetrieveZoneError(t *testing.T) {
|
||||
retrieveErr := errors.New("zone retrieval failed")
|
||||
|
||||
uc := orchestrator.NewZoneCorrectionListerUsecase(
|
||||
&mockProviderGetter{provider: &happydns.Provider{}},
|
||||
newTestListRecordsUsecase(),
|
||||
&mockZoneCorrector{},
|
||||
&mockZoneRetriever{err: retrieveErr},
|
||||
)
|
||||
|
||||
user := &happydns.User{Id: happydns.Identifier([]byte("test-user"))}
|
||||
domain := &happydns.Domain{
|
||||
ProviderId: happydns.Identifier([]byte("test-provider")),
|
||||
DomainName: "example.com.",
|
||||
}
|
||||
zone := &happydns.Zone{
|
||||
ZoneMeta: happydns.ZoneMeta{DefaultTTL: 3600},
|
||||
Services: map[happydns.Subdomain][]*happydns.Service{},
|
||||
}
|
||||
|
||||
_, _, err := uc.List(context.Background(), user, domain, zone)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !errors.Is(err, retrieveErr) {
|
||||
t.Errorf("expected %v, got %v", retrieveErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneCorrectionLister_List_NoCorrections(t *testing.T) {
|
||||
uc := orchestrator.NewZoneCorrectionListerUsecase(
|
||||
&mockProviderGetter{provider: &happydns.Provider{}},
|
||||
newTestListRecordsUsecase(),
|
||||
&mockZoneCorrector{corrections: nil, nbDiff: 0},
|
||||
&mockZoneRetriever{records: nil},
|
||||
)
|
||||
|
||||
user := &happydns.User{Id: happydns.Identifier([]byte("test-user"))}
|
||||
domain := &happydns.Domain{
|
||||
ProviderId: happydns.Identifier([]byte("test-provider")),
|
||||
DomainName: "example.com.",
|
||||
}
|
||||
zone := &happydns.Zone{
|
||||
ZoneMeta: happydns.ZoneMeta{DefaultTTL: 3600},
|
||||
Services: map[happydns.Subdomain][]*happydns.Service{},
|
||||
}
|
||||
|
||||
got, nbDiff, err := uc.List(context.Background(), user, domain, zone)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if nbDiff != 0 {
|
||||
t.Errorf("expected nbDiff=0, got %d", nbDiff)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Errorf("expected 0 corrections, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ package orchestrator
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
|
|
@ -30,24 +31,43 @@ import (
|
|||
"git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
// ZoneImporterUsecase converts a flat slice of DNS records into a structured
|
||||
// happyDomain zone, preserving metadata from the previous zone when available,
|
||||
// and persists the result as the newest entry in the domain's zone history.
|
||||
type ZoneImporterUsecase struct {
|
||||
domainUpdater DomainUpdater
|
||||
zoneCreator *zoneUC.CreateZoneUsecase
|
||||
zoneGetter *zoneUC.GetZoneUsecase
|
||||
}
|
||||
|
||||
func NewZoneImporterUsecase(domainUpdater DomainUpdater, zoneCreator *zoneUC.CreateZoneUsecase) *ZoneImporterUsecase {
|
||||
// NewZoneImporterUsecase creates a ZoneImporterUsecase with the given domain
|
||||
// updater, zone creator, and zone getter.
|
||||
func NewZoneImporterUsecase(domainUpdater DomainUpdater, zoneCreator *zoneUC.CreateZoneUsecase, zoneGetter *zoneUC.GetZoneUsecase) *ZoneImporterUsecase {
|
||||
return &ZoneImporterUsecase{
|
||||
domainUpdater: domainUpdater,
|
||||
zoneCreator: zoneCreator,
|
||||
zoneGetter: zoneGetter,
|
||||
}
|
||||
}
|
||||
|
||||
// Import analyzes rrs into services, optionally carries over metadata from the
|
||||
// domain's most recent zone, persists the new zone, and prepends its ID to the
|
||||
// domain's history. Returns the created zone or an error.
|
||||
func (uc *ZoneImporterUsecase) Import(user *happydns.User, domain *happydns.Domain, rrs []happydns.Record) (*happydns.Zone, error) {
|
||||
services, defaultTTL, err := svcs.AnalyzeZone(domain.DomainName, rrs)
|
||||
if err != nil {
|
||||
return nil, happydns.ValidationError{Msg: fmt.Sprintf("unable to perform the analysis of your zone: %s", err.Error())}
|
||||
}
|
||||
|
||||
if len(domain.ZoneHistory) > 0 {
|
||||
prevZone, err := uc.zoneGetter.Get(domain.ZoneHistory[0])
|
||||
if err != nil {
|
||||
log.Printf("ReassociateMetadata: unable to load previous zone %s: %s (metadata will not be transferred)", domain.ZoneHistory[0], err)
|
||||
} else {
|
||||
zoneUC.ReassociateMetadata(prevZone.Services, services, domain.DomainName, defaultTTL)
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
commit := fmt.Sprintf("Initial zone fetch from %s", domain.DomainName)
|
||||
if len(domain.ZoneHistory) > 0 {
|
||||
|
|
|
|||
30
internal/usecase/provider/doc.go
Normal file
30
internal/usecase/provider/doc.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package provider implements the use cases for DNS provider management.
|
||||
// Service handles creation, retrieval, update, and deletion of providers,
|
||||
// with ownership enforcement (a user may only access their own providers).
|
||||
// RestrictedService wraps Service and enforces the DisableProviders
|
||||
// configuration flag. Zone and domain operations (record retrieval,
|
||||
// correction listing, domain existence checking) are provided as separate
|
||||
// methods so they can be consumed by higher-level orchestration use cases
|
||||
// without circular imports.
|
||||
package provider
|
||||
|
|
@ -29,9 +29,9 @@ import (
|
|||
|
||||
// CreateDomainOnProvider creates a domain on the given provider.
|
||||
func (s *Service) CreateDomainOnProvider(provider *happydns.Provider, fqdn string) error {
|
||||
p, err := provider.InstantiateProvider()
|
||||
p, err := instantiate(provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to instantiate the provider: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !p.CanCreateDomain() {
|
||||
|
|
@ -41,20 +41,11 @@ func (s *Service) CreateDomainOnProvider(provider *happydns.Provider, fqdn strin
|
|||
return p.CreateDomain(fqdn)
|
||||
}
|
||||
|
||||
// CreateDomainOnProvider for RestrictedService enforces configuration restrictions.
|
||||
func (s *RestrictedService) CreateDomainOnProvider(provider *happydns.Provider, fqdn string) error {
|
||||
if s.config.DisableProviders {
|
||||
return happydns.ForbiddenError{Msg: "cannot create domain on provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.Service.CreateDomainOnProvider(provider, fqdn)
|
||||
}
|
||||
|
||||
// ListHostedDomains lists all domains hosted on the given provider.
|
||||
func (s *Service) ListHostedDomains(provider *happydns.Provider) ([]string, error) {
|
||||
p, err := provider.InstantiateProvider()
|
||||
p, err := instantiate(provider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to instantiate the provider: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !p.CanListZones() {
|
||||
|
|
@ -66,9 +57,9 @@ func (s *Service) ListHostedDomains(provider *happydns.Provider) ([]string, erro
|
|||
|
||||
// TestDomainExistence tests whether a domain exists on the given provider.
|
||||
func (s *Service) TestDomainExistence(provider *happydns.Provider, name string) error {
|
||||
instance, err := provider.InstantiateProvider()
|
||||
instance, err := instantiate(provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to instantiate provider: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = instance.GetZoneRecords(name)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
|
|
@ -29,23 +30,24 @@ import (
|
|||
"git.happydns.org/happyDomain/providers"
|
||||
)
|
||||
|
||||
// Service handles CRUD operations on DNS providers, with ownership enforcement.
|
||||
type Service struct {
|
||||
store ProviderStorage
|
||||
validator ProviderValidator
|
||||
}
|
||||
|
||||
func NewService(store ProviderStorage) *Service {
|
||||
// NewService creates a new provider Service. If validator is nil,
|
||||
// the DefaultProviderValidator is used.
|
||||
func NewService(store ProviderStorage, validator ProviderValidator) *Service {
|
||||
if validator == nil {
|
||||
validator = &DefaultProviderValidator{}
|
||||
}
|
||||
return &Service{
|
||||
store: store,
|
||||
validator: &DefaultProviderValidator{},
|
||||
validator: validator,
|
||||
}
|
||||
}
|
||||
|
||||
// SetValidator allows replacing the validator (useful for testing).
|
||||
func (s *Service) SetValidator(v ProviderValidator) {
|
||||
s.validator = v
|
||||
}
|
||||
|
||||
// ParseProvider converts a ProviderMessage to a Provider.
|
||||
func ParseProvider(msg *happydns.ProviderMessage) (p *happydns.Provider, err error) {
|
||||
p = &happydns.Provider{}
|
||||
|
|
@ -60,6 +62,15 @@ func ParseProvider(msg *happydns.ProviderMessage) (p *happydns.Provider, err err
|
|||
return
|
||||
}
|
||||
|
||||
// instantiate is a helper that instantiates a provider and wraps errors consistently.
|
||||
func instantiate(p *happydns.Provider) (happydns.ProviderActuator, error) {
|
||||
instance, err := p.InstantiateProvider()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to instantiate provider: %w", err)
|
||||
}
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// CreateProvider creates a new provider for the given user.
|
||||
func (s *Service) CreateProvider(user *happydns.User, msg *happydns.ProviderMessage) (*happydns.Provider, error) {
|
||||
provider, err := ParseProvider(msg)
|
||||
|
|
@ -121,7 +132,10 @@ func (s *Service) GetUserProviderMeta(user *happydns.User, providerID happydns.I
|
|||
func (s *Service) ListUserProviders(user *happydns.User) ([]*happydns.ProviderMeta, error) {
|
||||
items, err := s.store.ListProviders(user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list providers failed: %w", err)
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("failed to list providers: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to list your providers. Please try again later.",
|
||||
}
|
||||
}
|
||||
|
||||
metas := make([]*happydns.ProviderMeta, 0, len(items))
|
||||
|
|
@ -175,22 +189,11 @@ func (s *Service) UpdateProviderFromMessage(providerID happydns.Identifier, user
|
|||
|
||||
// DeleteProvider deletes a provider for the given user.
|
||||
func (s *Service) DeleteProvider(user *happydns.User, providerID happydns.Identifier) error {
|
||||
// TODO: Find another way to avoid import cycle
|
||||
// We should verify that no domains are using this provider before deleting
|
||||
/*domains, err := s.listDomains.List(user)
|
||||
if err != nil {
|
||||
return happydns.InternalError{
|
||||
Err: fmt.Errorf("failed to list domains: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to perform this action. Please try again later.",
|
||||
}
|
||||
// Verify ownership before deleting
|
||||
if _, err := s.getUserProvider(user, providerID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, d := range domains {
|
||||
if d.ProviderId.Equals(providerID) {
|
||||
return fmt.Errorf("You cannot delete this provider because it is still used by: %s", d.DomainName)
|
||||
}
|
||||
}*/
|
||||
|
||||
if err := s.store.DeleteProvider(providerID); err != nil {
|
||||
return happydns.InternalError{
|
||||
Err: fmt.Errorf("failed to delete provider %s: %w", providerID.String(), err),
|
||||
|
|
@ -201,48 +204,90 @@ func (s *Service) DeleteProvider(user *happydns.User, providerID happydns.Identi
|
|||
return nil
|
||||
}
|
||||
|
||||
// RestrictedService wraps Service with configuration-based restrictions.
|
||||
// RestrictedService wraps a ProviderUsecase with configuration-based restrictions.
|
||||
type RestrictedService struct {
|
||||
Service
|
||||
inner happydns.ProviderUsecase
|
||||
config *happydns.Options
|
||||
}
|
||||
|
||||
// NewRestrictedService creates a RestrictedService backed by the given configuration and storage.
|
||||
func NewRestrictedService(cfg *happydns.Options, store ProviderStorage) *RestrictedService {
|
||||
s := NewService(store)
|
||||
return &RestrictedService{
|
||||
*s,
|
||||
cfg,
|
||||
inner: NewService(store, nil),
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateProvider refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
||||
func (s *RestrictedService) CreateProvider(user *happydns.User, msg *happydns.ProviderMessage) (*happydns.Provider, error) {
|
||||
if s.config.DisableProviders {
|
||||
return nil, happydns.ForbiddenError{Msg: "cannot add provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.Service.CreateProvider(user, msg)
|
||||
return s.inner.CreateProvider(user, msg)
|
||||
}
|
||||
|
||||
// DeleteProvider refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
||||
func (s *RestrictedService) DeleteProvider(user *happydns.User, providerID happydns.Identifier) error {
|
||||
if s.config.DisableProviders {
|
||||
return happydns.ForbiddenError{Msg: "cannot delete provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.Service.DeleteProvider(user, providerID)
|
||||
return s.inner.DeleteProvider(user, providerID)
|
||||
}
|
||||
|
||||
// UpdateProvider refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
||||
func (s *RestrictedService) UpdateProvider(providerID happydns.Identifier, user *happydns.User, updateFn func(*happydns.Provider)) error {
|
||||
if s.config.DisableProviders {
|
||||
return happydns.ForbiddenError{Msg: "cannot update provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.Service.UpdateProvider(providerID, user, updateFn)
|
||||
return s.inner.UpdateProvider(providerID, user, updateFn)
|
||||
}
|
||||
|
||||
// UpdateProviderFromMessage refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
||||
func (s *RestrictedService) UpdateProviderFromMessage(providerID happydns.Identifier, user *happydns.User, p *happydns.ProviderMessage) error {
|
||||
if s.config.DisableProviders {
|
||||
return happydns.ForbiddenError{Msg: "cannot update provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.Service.UpdateProviderFromMessage(providerID, user, p)
|
||||
return s.inner.UpdateProviderFromMessage(providerID, user, p)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) CreateDomainOnProvider(provider *happydns.Provider, fqdn string) error {
|
||||
if s.config.DisableProviders {
|
||||
return happydns.ForbiddenError{Msg: "cannot create domain on provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.inner.CreateDomainOnProvider(provider, fqdn)
|
||||
}
|
||||
|
||||
// Read-only operations delegate directly.
|
||||
|
||||
func (s *RestrictedService) GetUserProvider(user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error) {
|
||||
return s.inner.GetUserProvider(user, providerID)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) GetUserProviderMeta(user *happydns.User, providerID happydns.Identifier) (*happydns.ProviderMeta, error) {
|
||||
return s.inner.GetUserProviderMeta(user, providerID)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) ListUserProviders(user *happydns.User) ([]*happydns.ProviderMeta, error) {
|
||||
return s.inner.ListUserProviders(user)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) ListHostedDomains(provider *happydns.Provider) ([]string, error) {
|
||||
return s.inner.ListHostedDomains(provider)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) ListZoneCorrections(ctx context.Context, provider *happydns.Provider, domain *happydns.Domain, records []happydns.Record) ([]*happydns.Correction, int, error) {
|
||||
return s.inner.ListZoneCorrections(ctx, provider, domain, records)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) RetrieveZone(ctx context.Context, provider *happydns.Provider, name string) ([]happydns.Record, error) {
|
||||
return s.inner.RetrieveZone(ctx, provider, name)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) TestDomainExistence(provider *happydns.Provider, name string) error {
|
||||
return s.inner.TestDomainExistence(provider, name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,12 +74,14 @@ func (v *mockValidator) Validate(p *happydns.Provider) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func Test_CreateProvider(t *testing.T) {
|
||||
func newTestService(t *testing.T) (*provider.Service, storage.Storage) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
// Replace validator with mock to avoid actual DNS validation
|
||||
providerService.SetValidator(&mockValidator{})
|
||||
return provider.NewService(db, &mockValidator{}), db
|
||||
}
|
||||
|
||||
func Test_CreateProvider(t *testing.T) {
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test DDNS Provider")
|
||||
|
|
@ -110,10 +112,7 @@ func Test_CreateProvider(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_GetUserProvider(t *testing.T) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
providerService.SetValidator(&mockValidator{})
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
|
||||
|
|
@ -139,10 +138,7 @@ func Test_GetUserProvider(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_GetUserProvider_WrongUser(t *testing.T) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
providerService.SetValidator(&mockValidator{})
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user1 := createTestUser(t, db, "user1@example.com")
|
||||
user2 := createTestUser(t, db, "user2@example.com")
|
||||
|
|
@ -165,9 +161,7 @@ func Test_GetUserProvider_WrongUser(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_GetUserProvider_NotFound(t *testing.T) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
|
||||
|
|
@ -182,10 +176,7 @@ func Test_GetUserProvider_NotFound(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_GetUserProviderMeta(t *testing.T) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
providerService.SetValidator(&mockValidator{})
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
|
||||
|
|
@ -211,10 +202,7 @@ func Test_GetUserProviderMeta(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_ListUserProviders(t *testing.T) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
providerService.SetValidator(&mockValidator{})
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
|
||||
|
|
@ -244,10 +232,7 @@ func Test_ListUserProviders(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_ListUserProviders_MultipleUsers(t *testing.T) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
providerService.SetValidator(&mockValidator{})
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user1 := createTestUser(t, db, "user1@example.com")
|
||||
user2 := createTestUser(t, db, "user2@example.com")
|
||||
|
|
@ -288,10 +273,7 @@ func Test_ListUserProviders_MultipleUsers(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_UpdateProvider(t *testing.T) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
providerService.SetValidator(&mockValidator{})
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
|
||||
|
|
@ -321,10 +303,7 @@ func Test_UpdateProvider(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_UpdateProvider_PreventIdChange(t *testing.T) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
providerService.SetValidator(&mockValidator{})
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
|
||||
|
|
@ -349,10 +328,7 @@ func Test_UpdateProvider_PreventIdChange(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_UpdateProvider_WrongUser(t *testing.T) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
providerService.SetValidator(&mockValidator{})
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user1 := createTestUser(t, db, "user1@example.com")
|
||||
user2 := createTestUser(t, db, "user2@example.com")
|
||||
|
|
@ -374,10 +350,7 @@ func Test_UpdateProvider_WrongUser(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_DeleteProvider(t *testing.T) {
|
||||
mem, _ := inmemory.NewInMemoryStorage()
|
||||
db, _ := kv.NewKVDatabase(mem)
|
||||
providerService := provider.NewService(db)
|
||||
providerService.SetValidator(&mockValidator{})
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
|
||||
|
|
@ -404,6 +377,35 @@ func Test_DeleteProvider(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_DeleteProvider_WrongUser(t *testing.T) {
|
||||
providerService, db := newTestService(t)
|
||||
|
||||
user1 := createTestUser(t, db, "user1@example.com")
|
||||
user2 := createTestUser(t, db, "user2@example.com")
|
||||
|
||||
// Create a provider for user1
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "User1 Provider")
|
||||
createdProvider, err := providerService.CreateProvider(user1, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
||||
// Try to delete the provider as user2
|
||||
err = providerService.DeleteProvider(user2, createdProvider.Id)
|
||||
if err == nil {
|
||||
t.Error("expected error when deleting another user's provider")
|
||||
}
|
||||
if err != happydns.ErrProviderNotFound {
|
||||
t.Errorf("expected ErrProviderNotFound, got %v", err)
|
||||
}
|
||||
|
||||
// Verify the provider still exists for user1
|
||||
_, err = providerService.GetUserProvider(user1, createdProvider.Id)
|
||||
if err != nil {
|
||||
t.Errorf("provider should still exist for user1, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ParseProvider(t *testing.T) {
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test Parse")
|
||||
|
||||
|
|
@ -462,8 +464,7 @@ func Test_RestrictedService_UpdateProvider_Disabled(t *testing.T) {
|
|||
db, _ := kv.NewKVDatabase(mem)
|
||||
|
||||
// First create a provider without restrictions
|
||||
unrestricted := provider.NewService(db)
|
||||
unrestricted.SetValidator(&mockValidator{})
|
||||
unrestricted := provider.NewService(db, &mockValidator{})
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
|
||||
createdProvider, err := unrestricted.CreateProvider(user, msg)
|
||||
|
|
@ -493,8 +494,7 @@ func Test_RestrictedService_DeleteProvider_Disabled(t *testing.T) {
|
|||
db, _ := kv.NewKVDatabase(mem)
|
||||
|
||||
// First create a provider without restrictions
|
||||
unrestricted := provider.NewService(db)
|
||||
unrestricted.SetValidator(&mockValidator{})
|
||||
unrestricted := provider.NewService(db, &mockValidator{})
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
|
||||
createdProvider, err := unrestricted.CreateProvider(user, msg)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// ProviderStorage is the persistence interface required by the provider use cases.
|
||||
type ProviderStorage interface {
|
||||
// ListAllProviders retrieves the list of known Providers.
|
||||
ListAllProviders() (happydns.Iterator[happydns.ProviderMessage], error)
|
||||
|
|
|
|||
|
|
@ -27,12 +27,15 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// ProviderValidator verifies that a provider configuration is functional before it is persisted.
|
||||
type ProviderValidator interface {
|
||||
Validate(*happydns.Provider) error
|
||||
}
|
||||
|
||||
// DefaultProviderValidator instantiates the provider and, when zone listing is supported, performs a live check.
|
||||
type DefaultProviderValidator struct{}
|
||||
|
||||
// Validate instantiates the provider and, if it supports zone listing, calls ListZones to confirm credentials are valid.
|
||||
func (v *DefaultProviderValidator) Validate(p *happydns.Provider) error {
|
||||
instance, err := p.InstantiateProvider()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -22,26 +22,26 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// RetrieveZone retrieves the current zone records for the given domain from the provider.
|
||||
func (s *Service) RetrieveZone(provider *happydns.Provider, name string) ([]happydns.Record, error) {
|
||||
instance, err := provider.InstantiateProvider()
|
||||
func (s *Service) RetrieveZone(_ context.Context, provider *happydns.Provider, name string) ([]happydns.Record, error) {
|
||||
instance, err := instantiate(provider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to instantiate provider: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return instance.GetZoneRecords(name)
|
||||
}
|
||||
|
||||
// ListZoneCorrections lists the corrections needed to synchronize the zone with the given records.
|
||||
func (s *Service) ListZoneCorrections(provider *happydns.Provider, domain *happydns.Domain, records []happydns.Record) ([]*happydns.Correction, int, error) {
|
||||
instance, err := provider.InstantiateProvider()
|
||||
func (s *Service) ListZoneCorrections(_ context.Context, provider *happydns.Provider, domain *happydns.Domain, records []happydns.Record) ([]*happydns.Correction, int, error) {
|
||||
instance, err := instantiate(provider)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("unable to instantiate provider: %w", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return instance.GetZoneCorrections(domain.DomainName, records)
|
||||
|
|
|
|||
|
|
@ -22,24 +22,22 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/forms"
|
||||
"git.happydns.org/happyDomain/internal/usecase/provider"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
type providerSettingsUsecase struct {
|
||||
config *happydns.Options
|
||||
providerService happydns.ProviderUsecase
|
||||
store provider.ProviderStorage
|
||||
}
|
||||
|
||||
func NewProviderSettingsUsecase(cfg *happydns.Options, ps happydns.ProviderUsecase, store provider.ProviderStorage) happydns.ProviderSettingsUsecase {
|
||||
func NewProviderSettingsUsecase(cfg *happydns.Options, ps happydns.ProviderUsecase) happydns.ProviderSettingsUsecase {
|
||||
return &providerSettingsUsecase{
|
||||
config: cfg,
|
||||
providerService: ps,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,56 +53,43 @@ func (psu *providerSettingsUsecase) NextProviderSettingsState(state *happydns.Pr
|
|||
return nil, nil, happydns.ForbiddenError{Msg: "cannot change provider settings as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
p, err := state.ProviderBody.InstantiateProvider()
|
||||
providerJSON, err := json.Marshal(state.ProviderBody)
|
||||
if err != nil {
|
||||
return nil, nil, happydns.ValidationError{Msg: fmt.Sprintf("unable to instantiate provider: %s", err.Error())}
|
||||
return nil, nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to marshal provider body: %w", err),
|
||||
UserMessage: happydns.TryAgainErr,
|
||||
}
|
||||
}
|
||||
|
||||
if p.CanListZones() {
|
||||
if _, err = p.ListZones(); err != nil {
|
||||
return nil, nil, happydns.ValidationError{Msg: fmt.Sprintf("unable to list provider's zones: %s", err.Error())}
|
||||
}
|
||||
msg := &happydns.ProviderMessage{
|
||||
ProviderMeta: happydns.ProviderMeta{
|
||||
Type: pType,
|
||||
Comment: state.Name,
|
||||
},
|
||||
Provider: providerJSON,
|
||||
}
|
||||
|
||||
if state.Id == nil {
|
||||
provider := &happydns.Provider{
|
||||
Provider: state.ProviderBody,
|
||||
ProviderMeta: happydns.ProviderMeta{
|
||||
Type: pType,
|
||||
Owner: user.Id,
|
||||
Comment: state.Name,
|
||||
},
|
||||
}
|
||||
// Create a new Provider
|
||||
err = psu.store.CreateProvider(provider)
|
||||
// Create a new Provider via the service layer
|
||||
provider, err := psu.providerService.CreateProvider(user, msg)
|
||||
if err != nil {
|
||||
return nil, nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to CreateProvider: %w", err),
|
||||
UserMessage: happydns.TryAgainErr,
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return provider, nil, nil
|
||||
} else {
|
||||
// Update an existing Provider
|
||||
p, err := psu.providerService.GetUserProvider(user, *state.Id)
|
||||
// Update an existing Provider via the service layer
|
||||
err := psu.providerService.UpdateProviderFromMessage(*state.Id, user, msg)
|
||||
if err != nil {
|
||||
return nil, nil, happydns.NotFoundError{Msg: fmt.Sprintf("unable to retrieve the original provider: %s", err.Error())}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
newp := &happydns.Provider{
|
||||
ProviderMeta: p.ProviderMeta,
|
||||
Provider: state.ProviderBody,
|
||||
}
|
||||
err = psu.store.UpdateProvider(newp)
|
||||
provider, err := psu.providerService.GetUserProvider(user, *state.Id)
|
||||
if err != nil {
|
||||
return nil, nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to UpdateProvider: %w", err),
|
||||
UserMessage: happydns.TryAgainErr,
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return newp, nil, nil
|
||||
return provider, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,18 +19,32 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package service implements use cases that operate on individual DNS services
|
||||
// (the logical groupings of records within a zone subdomain). It provides:
|
||||
// - ListRecordsUsecase – expands a Service into its constituent DNS records.
|
||||
// - SearchRecordUsecase – locates the Service and subdomain that owns a given
|
||||
// record within a Zone.
|
||||
// - ValidateServiceUsecase – verifies that a ServiceBody can generate at least
|
||||
// one record and returns a SHA-1 hash of the resulting RDATA.
|
||||
// - ParseService – deserialises a ServiceMessage into a typed Service value.
|
||||
//
|
||||
// The Service facade wires these together and is the main entry point consumed
|
||||
// by higher-level zone use cases.
|
||||
package service
|
||||
|
||||
import (
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// Service is the facade for all service-level use cases. Callers should use
|
||||
// its methods rather than reaching into the embedded use-case structs directly.
|
||||
type Service struct {
|
||||
ListRecordsUC *ListRecordsUsecase
|
||||
SearchRecordUC *SearchRecordUsecase
|
||||
ValidateServiceUC *ValidateServiceUsecase
|
||||
}
|
||||
|
||||
// NewServiceUsecases wires and returns a ready-to-use Service facade.
|
||||
func NewServiceUsecases() *Service {
|
||||
ListRecordsUC := NewListRecordsUsecase()
|
||||
|
||||
|
|
@ -41,10 +55,14 @@ func NewServiceUsecases() *Service {
|
|||
}
|
||||
}
|
||||
|
||||
// ListRecords expands the given service into its constituent DNS records,
|
||||
// qualifying names relative to domain and applying the zone's default TTL.
|
||||
func (s *Service) ListRecords(domain *happydns.Domain, zone *happydns.Zone, service *happydns.Service) ([]happydns.Record, error) {
|
||||
return s.ListRecordsUC.List(service, domain.DomainName, zone.DefaultTTL)
|
||||
}
|
||||
|
||||
// ValidateService verifies that body generates at least one DNS record and
|
||||
// returns a SHA-1 hash of the resulting RDATA for change-detection purposes.
|
||||
func (s *Service) ValidateService(body happydns.ServiceBody, subdomain happydns.Subdomain, origin happydns.Origin) ([]byte, error) {
|
||||
return s.ValidateServiceUC.Validate(body, subdomain, origin)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,12 +26,18 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// ListRecordsUsecase expands a Service into its raw DNS records.
|
||||
type ListRecordsUsecase struct{}
|
||||
|
||||
// NewListRecordsUsecase returns a new ListRecordsUsecase.
|
||||
func NewListRecordsUsecase() *ListRecordsUsecase {
|
||||
return &ListRecordsUsecase{}
|
||||
}
|
||||
|
||||
// List generates the DNS records produced by svc. Record names are made
|
||||
// absolute relative to origin and any record whose TTL is zero inherits
|
||||
// defaultTTL. When svc.Ttl is non-zero it overrides defaultTTL for all
|
||||
// records of this service.
|
||||
func (uc *ListRecordsUsecase) List(svc *happydns.Service, origin string, defaultTTL uint32) ([]happydns.Record, error) {
|
||||
if svc.Ttl != 0 {
|
||||
defaultTTL = svc.Ttl
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ import (
|
|||
"git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
// ParseService deserialises a ServiceMessage into a typed Service value.
|
||||
// It looks up the concrete ServiceBody type by msg.Type, then JSON-decodes
|
||||
// msg.Service into it.
|
||||
func ParseService(msg *happydns.ServiceMessage) (svc *happydns.Service, err error) {
|
||||
svc = &happydns.Service{}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,16 +27,22 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// SearchRecordUsecase locates the Service and subdomain that own a given DNS
|
||||
// record within a Zone.
|
||||
type SearchRecordUsecase struct {
|
||||
serviceListRecordsUC *ListRecordsUsecase
|
||||
}
|
||||
|
||||
// NewSearchRecordUsecase returns a SearchRecordUsecase backed by the provided
|
||||
// ListRecordsUsecase.
|
||||
func NewSearchRecordUsecase(serviceListRecordsUC *ListRecordsUsecase) *SearchRecordUsecase {
|
||||
return &SearchRecordUsecase{
|
||||
serviceListRecordsUC: serviceListRecordsUC,
|
||||
}
|
||||
}
|
||||
|
||||
// ExistsInService reports whether record is produced by svc. Two records are
|
||||
// considered equal when their name, type, class, and RDATA all match.
|
||||
func (uc *SearchRecordUsecase) ExistsInService(svc *happydns.Service, record happydns.Record) (bool, error) {
|
||||
records, err := uc.serviceListRecordsUC.List(svc, "", 0)
|
||||
if err != nil {
|
||||
|
|
@ -55,6 +61,9 @@ func (uc *SearchRecordUsecase) ExistsInService(svc *happydns.Service, record hap
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// Search scans every subdomain in zone and returns the first subdomain and
|
||||
// Service that produce record. Returns an empty subdomain and nil service
|
||||
// when no match is found.
|
||||
func (uc *SearchRecordUsecase) Search(zone *happydns.Zone, record happydns.Record) (happydns.Subdomain, *happydns.Service, error) {
|
||||
for dn, _ := range zone.Services {
|
||||
svc, err := uc.SearchInSubdomain(zone, dn, record)
|
||||
|
|
@ -66,6 +75,8 @@ func (uc *SearchRecordUsecase) Search(zone *happydns.Zone, record happydns.Recor
|
|||
return "", nil, nil
|
||||
}
|
||||
|
||||
// SearchInSubdomain looks for record among the services attached to subdomain
|
||||
// in zone. Returns nil when subdomain does not exist or no service matches.
|
||||
func (uc *SearchRecordUsecase) SearchInSubdomain(zone *happydns.Zone, subdomain happydns.Subdomain, record happydns.Record) (*happydns.Service, error) {
|
||||
services, ok := zone.Services[subdomain]
|
||||
if !ok {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// ZoneUpdaterStorage is the storage interface required by use cases that need
|
||||
// to persist changes to a Zone.
|
||||
type ZoneUpdaterStorage interface {
|
||||
// UpdateZone updates the fields of the given Zone.
|
||||
UpdateZone(zone *happydns.Zone) error
|
||||
|
|
|
|||
|
|
@ -29,12 +29,19 @@ import (
|
|||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// ValidateServiceUsecase verifies that a ServiceBody can produce at least one
|
||||
// DNS record and computes a SHA-1 fingerprint of the resulting RDATA.
|
||||
type ValidateServiceUsecase struct{}
|
||||
|
||||
// NewValidateServiceUsecase returns a new ValidateServiceUsecase.
|
||||
func NewValidateServiceUsecase() *ValidateServiceUsecase {
|
||||
return &ValidateServiceUsecase{}
|
||||
}
|
||||
|
||||
// Validate calls svc.GetRecords with the given subdomain and origin. It
|
||||
// returns an error when no records are generated, otherwise it returns a
|
||||
// SHA-1 hash of all record strings concatenated — suitable for change
|
||||
// detection on the client side.
|
||||
func (uc *ValidateServiceUsecase) Validate(svc happydns.ServiceBody, subdomain happydns.Subdomain, origin happydns.Origin) ([]byte, error) {
|
||||
rrs, err := svc.GetRecords(string(subdomain), 0, string(origin))
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -33,13 +33,19 @@ import (
|
|||
"git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
// serviceSpecsUsecase implements happydns.ServiceSpecsUsecase, providing
|
||||
// introspection into registered DNS services: listing them, retrieving their
|
||||
// field specifications, and generating preview DNS records.
|
||||
type serviceSpecsUsecase struct {
|
||||
}
|
||||
|
||||
// NewServiceSpecsUsecase creates a new ServiceSpecsUsecase.
|
||||
func NewServiceSpecsUsecase() happydns.ServiceSpecsUsecase {
|
||||
return &serviceSpecsUsecase{}
|
||||
}
|
||||
|
||||
// ListServices returns metadata (ServiceInfos) for every registered DNS service,
|
||||
// keyed by service type identifier.
|
||||
func (ssu *serviceSpecsUsecase) ListServices() map[string]happydns.ServiceInfos {
|
||||
services := svcs.ListServices()
|
||||
|
||||
|
|
@ -51,6 +57,9 @@ func (ssu *serviceSpecsUsecase) ListServices() map[string]happydns.ServiceInfos
|
|||
return ret
|
||||
}
|
||||
|
||||
// GetServiceIcon returns the raw PNG icon bytes for the service identified by
|
||||
// ssid (with or without the ".png" suffix). Returns NotFoundError if no icon
|
||||
// is registered for that service.
|
||||
func (ssu *serviceSpecsUsecase) GetServiceIcon(ssid string) ([]byte, error) {
|
||||
cnt, ok := svcs.Icons[strings.TrimSuffix(ssid, ".png")]
|
||||
if !ok {
|
||||
|
|
@ -60,11 +69,19 @@ func (ssu *serviceSpecsUsecase) GetServiceIcon(ssid string) ([]byte, error) {
|
|||
return cnt, nil
|
||||
}
|
||||
|
||||
// GetServiceSpecs returns the field specifications for a service type,
|
||||
// describing each configurable field with its type, label, constraints, and
|
||||
// other UI metadata.
|
||||
func (ssu *serviceSpecsUsecase) GetServiceSpecs(svctype reflect.Type) (*happydns.ServiceSpecs, error) {
|
||||
return ssu.getSpecs(svctype)
|
||||
}
|
||||
|
||||
func (ssu *serviceSpecsUsecase) InitializeService(svctype reflect.Type) (interface{}, error) {
|
||||
// InitializeService returns a new instance of the service type populated with
|
||||
// sensible default values. If the service implements ServiceInitializer its
|
||||
// Initialize method is called; otherwise defaults are derived by reflection:
|
||||
// slices of scalar types are pre-populated with one empty element, nested
|
||||
// structs and DNS record types are recursively initialized.
|
||||
func (ssu *serviceSpecsUsecase) InitializeService(svctype reflect.Type) (any, error) {
|
||||
// Create a new instance of the service
|
||||
svcPtr := reflect.New(svctype)
|
||||
svc := svcPtr.Interface()
|
||||
|
|
@ -110,6 +127,7 @@ func (ssu *serviceSpecsUsecase) InitializeService(svctype reflect.Type) (interfa
|
|||
return svc, nil
|
||||
}
|
||||
|
||||
// countSettableFields returns the number of exported, non-anonymous fields in v.
|
||||
func (ssu *serviceSpecsUsecase) countSettableFields(v reflect.Value) int {
|
||||
count := 0
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
|
|
@ -123,6 +141,10 @@ func (ssu *serviceSpecsUsecase) countSettableFields(v reflect.Value) int {
|
|||
return count
|
||||
}
|
||||
|
||||
// initializeStructFields recursively initializes exported fields of a struct
|
||||
// value: slices become empty non-nil slices, maps become empty non-nil maps,
|
||||
// DNS types are initialized via initializeDNSRecord, and nested structs are
|
||||
// processed recursively.
|
||||
func (ssu *serviceSpecsUsecase) initializeStructFields(v reflect.Value) {
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
|
|
@ -174,7 +196,9 @@ func (ssu *serviceSpecsUsecase) initializeStructFields(v reflect.Value) {
|
|||
}
|
||||
}
|
||||
|
||||
// isDNSType checks if a type is from the miekg/dns package or a happyDomain DNS abstraction
|
||||
// isDNSType reports whether t is a DNS record type — either from the
|
||||
// github.com/miekg/dns package or a happyDomain model type that embeds a
|
||||
// dns.RR_Header field named "Hdr".
|
||||
func (ssu *serviceSpecsUsecase) isDNSType(t reflect.Type) bool {
|
||||
pkgPath := t.PkgPath()
|
||||
|
||||
|
|
|
|||
51
internal/usecase/session/doc.go
Normal file
51
internal/usecase/session/doc.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package session provides the business logic for managing user sessions in
|
||||
// happyDomain. It exposes a [Service] that handles the full session lifecycle:
|
||||
// creation, retrieval, update, and deletion, as well as bulk operations such as
|
||||
// closing all sessions for a given user.
|
||||
//
|
||||
// The package defines the [SessionStorage] interface that any persistence
|
||||
// backend must implement. A concrete implementation is injected at construction
|
||||
// time via [NewService], keeping this layer free of storage concerns.
|
||||
//
|
||||
// Session identifiers are randomly generated, base32-encoded strings (see
|
||||
// [NewSessionID]). Sessions carry an expiry timestamp and are automatically
|
||||
// bound to a single user — cross-user access is rejected at the use-case level.
|
||||
//
|
||||
// Typical usage:
|
||||
//
|
||||
// svc := session.NewService(myStorageBackend)
|
||||
//
|
||||
// sess, err := svc.CreateUserSession(user, "browser login")
|
||||
// // … store sess.Id in a cookie …
|
||||
//
|
||||
// sess, err = svc.GetUserSession(user, sessionID)
|
||||
//
|
||||
// err = svc.UpdateUserSession(user, sessionID, func(s *happydns.Session) {
|
||||
// s.Description = "renamed"
|
||||
// })
|
||||
//
|
||||
// err = svc.DeleteUserSession(user, sessionID)
|
||||
//
|
||||
// err = svc.CloseUserSessions(user) // invalidate all sessions at once
|
||||
package session
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue