mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-04 01:00:17 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb48f1eb2d | |||
| 33a5b859cf | |||
| 6ab9c0a0c5 | |||
| 4aa05aeb79 | |||
| 440a3a3ef5 | |||
| a3c4d6ac83 | |||
| c8b31c54a0 | |||
| 04b93fa107 | |||
| a6c716c4e2 | |||
| ffafb5bff5 | |||
| bb867ea5f4 |
+17
-17
@@ -220,6 +220,23 @@ TINYAUTH_LDAP_AUTHCERT=
|
|||||||
TINYAUTH_LDAP_AUTHKEY=
|
TINYAUTH_LDAP_AUTHKEY=
|
||||||
# Cache duration for LDAP group membership in seconds.
|
# Cache duration for LDAP group membership in seconds.
|
||||||
TINYAUTH_LDAP_GROUPCACHETTL=900
|
TINYAUTH_LDAP_GROUPCACHETTL=900
|
||||||
|
|
||||||
|
# experimental config
|
||||||
|
|
||||||
|
# Enable Tailscale integration.
|
||||||
|
TINYAUTH_EXPERIMENTAL_TAILSCALE_ENABLED=false
|
||||||
|
# Tailscale state directory.
|
||||||
|
TINYAUTH_EXPERIMENTAL_TAILSCALE_DIR="./tailscale_state"
|
||||||
|
# Tailscale hostname.
|
||||||
|
TINYAUTH_EXPERIMENTAL_TAILSCALE_HOSTNAME=
|
||||||
|
# Tailscale auth key.
|
||||||
|
TINYAUTH_EXPERIMENTAL_TAILSCALE_AUTHKEY=
|
||||||
|
# Use ephemeral Tailscale node.
|
||||||
|
TINYAUTH_EXPERIMENTAL_TAILSCALE_EPHEMERAL=false
|
||||||
|
# Enable Tailscale Funnel.
|
||||||
|
TINYAUTH_EXPERIMENTAL_TAILSCALE_FUNNEL=false
|
||||||
|
# Listen on the Tailscale address instead of standard address.
|
||||||
|
TINYAUTH_EXPERIMENTAL_TAILSCALE_LISTEN=false
|
||||||
# Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment.
|
# Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment.
|
||||||
TINYAUTH_LABELPROVIDER="auto"
|
TINYAUTH_LABELPROVIDER="auto"
|
||||||
|
|
||||||
@@ -241,20 +258,3 @@ TINYAUTH_LOG_STREAMS_APP_LEVEL=
|
|||||||
TINYAUTH_LOG_STREAMS_AUDIT_ENABLED=false
|
TINYAUTH_LOG_STREAMS_AUDIT_ENABLED=false
|
||||||
# Log level for this stream. Use global if empty.
|
# Log level for this stream. Use global if empty.
|
||||||
TINYAUTH_LOG_STREAMS_AUDIT_LEVEL=
|
TINYAUTH_LOG_STREAMS_AUDIT_LEVEL=
|
||||||
|
|
||||||
# tailscale config
|
|
||||||
|
|
||||||
# Enable Tailscale integration.
|
|
||||||
TINYAUTH_TAILSCALE_ENABLED=false
|
|
||||||
# Tailscale state directory.
|
|
||||||
TINYAUTH_TAILSCALE_DIR="./tailscale_state"
|
|
||||||
# Tailscale hostname.
|
|
||||||
TINYAUTH_TAILSCALE_HOSTNAME=
|
|
||||||
# Tailscale auth key.
|
|
||||||
TINYAUTH_TAILSCALE_AUTHKEY=
|
|
||||||
# Use ephemeral Tailscale node.
|
|
||||||
TINYAUTH_TAILSCALE_EPHEMERAL=false
|
|
||||||
# Enable Tailscale Funnel.
|
|
||||||
TINYAUTH_TAILSCALE_FUNNEL=false
|
|
||||||
# Listen on the Tailscale address instead of standard address.
|
|
||||||
TINYAUTH_TAILSCALE_LISTEN=false
|
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ jobs:
|
|||||||
- name: Check codegen is up to date
|
- name: Check codegen is up to date
|
||||||
run: |
|
run: |
|
||||||
sqlc generate
|
sqlc generate
|
||||||
go generate ./internal/repository/...
|
go generate ./...
|
||||||
git diff --exit-code -- internal/repository/
|
git diff --exit-code
|
||||||
git status --porcelain -- internal/repository/ | grep -q . && echo "untracked files in internal/repository/" && exit 1 || true
|
git status --porcelain | grep -q . && echo "untracked files code gen files" && exit 1 || true
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
|
|||||||
@@ -51,3 +51,6 @@ config.certify.yml
|
|||||||
|
|
||||||
# deepsec
|
# deepsec
|
||||||
/.deepsec
|
/.deepsec
|
||||||
|
|
||||||
|
# jetbrains
|
||||||
|
/.idea/
|
||||||
|
|||||||
+7
-5
@@ -52,15 +52,17 @@ WORKDIR /tinyauth
|
|||||||
|
|
||||||
COPY --from=builder /tinyauth/tinyauth ./
|
COPY --from=builder /tinyauth/tinyauth ./
|
||||||
|
|
||||||
RUN mkdir -p /data
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Make the data directory with a non-root user
|
||||||
|
RUN addgroup tinyauth && adduser -DH tinyauth -G tinyauth
|
||||||
|
RUN mkdir -p /data/resources /data/oidc /data/tailscale
|
||||||
|
RUN chown -R tinyauth:tinyauth /data
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
|
# Tell tinyauth that it's running in a container and where to find the data directory
|
||||||
|
ENV RUNTIME_ENV=docker
|
||||||
ENV TINYAUTH_RESOURCES_PATH=/data/resources
|
|
||||||
|
|
||||||
ENV PATH=$PATH:/tinyauth
|
ENV PATH=$PATH:/tinyauth
|
||||||
|
|
||||||
|
|||||||
@@ -40,13 +40,16 @@ COPY ./cmd ./cmd
|
|||||||
COPY ./internal ./internal
|
COPY ./internal ./internal
|
||||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||||
|
|
||||||
RUN mkdir -p data
|
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "${LDFLAGS} \
|
RUN CGO_ENABLED=0 go build -ldflags "${LDFLAGS} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
|
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||||
|
|
||||||
|
# Make the data directory with a non-root user
|
||||||
|
RUN addgroup tinyauth && adduser -DH tinyauth -G tinyauth
|
||||||
|
RUN mkdir -p /data/resources /data/oidc /data/tailscale
|
||||||
|
RUN chown -R tinyauth:tinyauth /data
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM gcr.io/distroless/static-debian12:latest AS runner
|
FROM gcr.io/distroless/static-debian12:latest AS runner
|
||||||
|
|
||||||
@@ -55,15 +58,14 @@ WORKDIR /tinyauth
|
|||||||
COPY --from=builder /tinyauth/tinyauth ./
|
COPY --from=builder /tinyauth/tinyauth ./
|
||||||
|
|
||||||
# Since it's distroless, we need to copy the data directory from the builder stage
|
# Since it's distroless, we need to copy the data directory from the builder stage
|
||||||
COPY --from=builder /tinyauth/data /data
|
COPY --from=builder /data /data
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
|
# Tell tinyauth that it's running in a container and where to find the data directory
|
||||||
|
ENV RUNTIME_ENV=docker
|
||||||
ENV TINYAUTH_RESOURCES_PATH=/data/resources
|
|
||||||
|
|
||||||
ENV PATH=$PATH:/tinyauth
|
ENV PATH=$PATH:/tinyauth
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c
|
|||||||
|
|
||||||
.DEFAULT_GOAL := binary
|
.DEFAULT_GOAL := binary
|
||||||
|
|
||||||
|
.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless swagger swagger-fmt
|
||||||
|
|
||||||
# Deps
|
# Deps
|
||||||
deps:
|
deps:
|
||||||
cd frontend && pnpm ci
|
cd frontend && pnpm ci
|
||||||
@@ -58,12 +60,10 @@ binary-linux-arm64:
|
|||||||
$(MAKE) binary
|
$(MAKE) binary
|
||||||
|
|
||||||
# Go test
|
# Go test
|
||||||
.PHONY: test
|
|
||||||
test:
|
test:
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
|
|
||||||
# Go vet
|
# Go vet
|
||||||
.PHONY: vet
|
|
||||||
vet:
|
vet:
|
||||||
go vet ./...
|
go vet ./...
|
||||||
|
|
||||||
@@ -88,11 +88,25 @@ prod-infisical:
|
|||||||
infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||||
|
|
||||||
# SQL
|
# SQL
|
||||||
.PHONY: sql
|
|
||||||
sql:
|
sql:
|
||||||
sqlc generate
|
sqlc generate
|
||||||
|
|
||||||
# Go gen
|
# Go gen
|
||||||
generate:
|
generate:
|
||||||
go run ./gen
|
go generate ./...
|
||||||
go generate ./internal/repository/...
|
|
||||||
|
# Docker image
|
||||||
|
docker:
|
||||||
|
docker buildx build -t tinyauthapp/tinyauth:dev --build-arg=VERSION=$(TAG_NAME) --build-arg=COMMIT_HASH=$(COMMIT_HASH) --build-arg=BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) -f Dockerfile .
|
||||||
|
|
||||||
|
# Docker image distroless
|
||||||
|
docker-distroless:
|
||||||
|
docker buildx build -t tinyauthapp/tinyauth:dev-distroless --build-arg=VERSION=$(TAG_NAME) --build-arg=COMMIT_HASH=$(COMMIT_HASH) --build-arg=BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) -f Dockerfile.distroless .
|
||||||
|
|
||||||
|
# Swagger
|
||||||
|
swagger:
|
||||||
|
swag init -d ./internal -g bootstrap/router_bootstrap.go -o ./internal/swagger
|
||||||
|
|
||||||
|
# Swagger Format
|
||||||
|
swagger-fmt:
|
||||||
|
swag fmt -d ./internal -g bootstrap/router_bootstrap.go
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png">
|
<img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png">
|
||||||
<h1>Tinyauth</h1>
|
<h1>Tinyauth</h1>
|
||||||
<p>The tiniest authentication and authorization server you have ever seen.</p>
|
<p>The tiniest OpenID Certified™ authorization and authentication server you have ever seen.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -28,6 +28,10 @@ Tinyauth is the simplest and tiniest authentication and authorization server you
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> This is the main development branch. For the latest stable release, see the [documentation](https://tinyauth.app) or the latest stable tag.
|
> This is the main development branch. For the latest stable release, see the [documentation](https://tinyauth.app) or the latest stable tag.
|
||||||
|
|
||||||
|
As of 2026-06-25, Tinyauth v5.1.0 is OpenID Certified™ for Basic OP. You can find the certification details [here](https://openid.net/certification-old/certified-openid-providers-profiles/), test suite available [here](https://www.certification.openid.net/plan-detail.html?public=true&plan=H0qhpsOcQkxUE).
|
||||||
|
|
||||||
|
<img alt="OpenID Certified" width="200" src="https://openid.net/wordpress-content/uploads/2016/05/oid-l-certification-mark-l-cmyk-150dpi-90mm.jpg" />
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
You can get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker-compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities (keep in mind that this file lives in the development branch so it may have updates that are not yet released).
|
You can get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker-compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities (keep in mind that this file lives in the development branch so it may have updates that are not yet released).
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func configCmd(tconfig *model.Config, loaders []cli.ResourceLoader) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "config",
|
||||||
|
Description: "Dump the current configuration in YAML format, useful for debugging",
|
||||||
|
Configuration: tconfig,
|
||||||
|
Resources: loaders,
|
||||||
|
Run: func(_ []string) error {
|
||||||
|
buf := strings.Builder{}
|
||||||
|
|
||||||
|
fmt.Fprint(&buf, "Your current configuration in YAML is:\n\n")
|
||||||
|
|
||||||
|
err := renderYamlToBuf(&buf, tconfig)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print(buf.String())
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createOidcClientCmd() *cli.Command {
|
func createOidcClientCmd() *cli.Command {
|
||||||
@@ -31,40 +32,84 @@ func createOidcClientCmd() *cli.Command {
|
|||||||
return errors.New("client name can only contain alphanumeric characters and hyphens")
|
return errors.New("client name can only contain alphanumeric characters and hyphens")
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid := uuid.New()
|
u := uuid.New()
|
||||||
clientId := uuid.String()
|
clientId := u.String()
|
||||||
clientSecret := "ta-" + utils.GenerateString(61)
|
clientSecret := "ta-" + utils.GenerateString(61)
|
||||||
|
|
||||||
uclientName := strings.ToUpper(clientName)
|
uclientName := strings.ToUpper(clientName)
|
||||||
lclientName := strings.ToLower(clientName)
|
lclientName := strings.ToLower(clientName)
|
||||||
|
|
||||||
builder := strings.Builder{}
|
buf := strings.Builder{}
|
||||||
|
|
||||||
// header
|
// header
|
||||||
fmt.Fprintf(&builder, "Created credentials for client %s\n\n", clientName)
|
fmt.Fprintf(&buf, "Created '%s' OIDC client.\n\n", clientName)
|
||||||
|
|
||||||
// credentials
|
// credentials
|
||||||
fmt.Fprintf(&builder, "Client Name: %s\n", clientName)
|
fmt.Fprintf(&buf, "Credentials:\n\n")
|
||||||
fmt.Fprintf(&builder, "Client ID: %s\n", clientId)
|
fmt.Fprintf(&buf, "Client Name: %s\n", clientName)
|
||||||
fmt.Fprintf(&builder, "Client Secret: %s\n\n", clientSecret)
|
fmt.Fprintf(&buf, "Client ID: %s\n", clientId)
|
||||||
|
fmt.Fprintf(&buf, "Client Secret: %s\n\n", clientSecret)
|
||||||
|
|
||||||
// env variables
|
// end variables
|
||||||
fmt.Fprint(&builder, "Environment variables:\n\n")
|
fmt.Fprintf(&buf, "Environment variables:\n\n")
|
||||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTID=%s\n", uclientName, clientId)
|
renderToBuf(&buf, []kv{
|
||||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTSECRET=%s\n", uclientName, clientSecret)
|
{
|
||||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_NAME=%s\n\n", uclientName, utils.Capitalize(lclientName))
|
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_CLIENTID", uclientName),
|
||||||
|
v: clientId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_CLIENTSECRET", uclientName),
|
||||||
|
v: clientSecret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_NAME", uclientName),
|
||||||
|
v: utils.Capitalize(lclientName),
|
||||||
|
},
|
||||||
|
}, "=")
|
||||||
|
fmt.Fprintf(&buf, "\n")
|
||||||
|
|
||||||
// cli flags
|
// cli flags
|
||||||
fmt.Fprint(&builder, "CLI flags:\n\n")
|
fmt.Fprintf(&buf, "CLI flags:\n\n")
|
||||||
fmt.Fprintf(&builder, "--oidc.clients.%s.clientid=%s\n", lclientName, clientId)
|
renderToBuf(&buf, []kv{
|
||||||
fmt.Fprintf(&builder, "--oidc.clients.%s.clientsecret=%s\n", lclientName, clientSecret)
|
{
|
||||||
fmt.Fprintf(&builder, "--oidc.clients.%s.name=%s\n\n", lclientName, utils.Capitalize(lclientName))
|
k: fmt.Sprintf("--oidc.clients.%s.clientid", lclientName),
|
||||||
|
v: clientId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: fmt.Sprintf("--oidc.clients.%s.clientsecret", lclientName),
|
||||||
|
v: clientSecret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: fmt.Sprintf("--oidc.clients.%s.name", lclientName),
|
||||||
|
v: utils.Capitalize(lclientName),
|
||||||
|
},
|
||||||
|
}, "=")
|
||||||
|
fmt.Fprintf(&buf, "\n")
|
||||||
|
|
||||||
|
// yaml config
|
||||||
|
fmt.Fprintf(&buf, "YAML config:\n\n")
|
||||||
|
|
||||||
|
err = renderYamlToBuf(&buf, &model.OIDCConfig{
|
||||||
|
Clients: map[string]model.OIDCClientConfig{
|
||||||
|
lclientName: {
|
||||||
|
ClientID: clientId,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
Name: utils.Capitalize(lclientName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString("\n")
|
||||||
|
|
||||||
// footer
|
// footer
|
||||||
fmt.Fprintln(&builder, "You can use either option to configure your OIDC client. Make sure to save these credentials as there is no way to regenerate them.")
|
fmt.Fprintln(&buf, "You can use any of the above options to configure your OIDC client. Make sure to save these credentials as there is no way to regenerate them.")
|
||||||
|
|
||||||
// print
|
// print
|
||||||
out := builder.String()
|
out := buf.String()
|
||||||
fmt.Print(out)
|
fmt.Print(out)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
+100
-54
@@ -3,11 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,62 +35,107 @@ func createUserCmd() *cli.Command {
|
|||||||
&cli.FlagLoader{},
|
&cli.FlagLoader{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cli.Command{
|
cmd := &cli.Command{
|
||||||
Name: "create",
|
Name: "create",
|
||||||
Description: "Create a user",
|
Description: "Create a user",
|
||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
|
||||||
log := logger.NewLogger().WithSimpleConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
if tCfg.Interactive {
|
|
||||||
form := huh.NewForm(
|
|
||||||
huh.NewGroup(
|
|
||||||
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
|
|
||||||
if s == "" {
|
|
||||||
return errors.New("username cannot be empty")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})),
|
|
||||||
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
|
|
||||||
if s == "" {
|
|
||||||
return errors.New("password cannot be empty")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})),
|
|
||||||
huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&tCfg.Docker),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
theme := new(themeBase)
|
|
||||||
err := form.WithTheme(theme).Run()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tCfg.Username == "" || tCfg.Password == "" {
|
|
||||||
return errors.New("username and password cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.App.Info().Str("username", tCfg.Username).Msg("Creating user")
|
|
||||||
|
|
||||||
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to hash password: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If docker format is enabled, escape the dollar sign
|
|
||||||
passwdStr := string(passwd)
|
|
||||||
if tCfg.Docker {
|
|
||||||
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.App.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Run = func(_ []string) error {
|
||||||
|
if tCfg.Interactive {
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate(func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return errors.New("username cannot be empty")
|
||||||
|
}
|
||||||
|
if strings.Contains(s, ":") {
|
||||||
|
return errors.New("username cannot contain ':'")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate(func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return errors.New("password cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&tCfg.Docker),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
theme := new(themeBase)
|
||||||
|
|
||||||
|
err := form.WithTheme(theme).Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tCfg.Username == "" || tCfg.Password == "" {
|
||||||
|
cmd.PrintHelp(os.Stdout)
|
||||||
|
return errors.New("username and password cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(tCfg.Username, ":") {
|
||||||
|
return errors.New("username cannot contain ':'")
|
||||||
|
}
|
||||||
|
|
||||||
|
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to hash password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the docker compose output needs $ escaped, the raw hash is correct everywhere else
|
||||||
|
passwdStr := string(passwd)
|
||||||
|
outputStr := passwdStr
|
||||||
|
|
||||||
|
if tCfg.Docker {
|
||||||
|
outputStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)
|
||||||
|
escapedUser := fmt.Sprintf("%s:%s", tCfg.Username, outputStr)
|
||||||
|
|
||||||
|
buf := strings.Builder{}
|
||||||
|
|
||||||
|
// header
|
||||||
|
fmt.Fprintf(&buf, "Created user '%s'.\n\n", tCfg.Username)
|
||||||
|
|
||||||
|
// environment variable
|
||||||
|
fmt.Fprint(&buf, "Environment variable:\n\n")
|
||||||
|
renderToBuf(&buf, []kv{
|
||||||
|
{"TINYAUTH_AUTH_USERS", escapedUser},
|
||||||
|
}, "=")
|
||||||
|
|
||||||
|
// cli flags
|
||||||
|
fmt.Fprint(&buf, "\nCLI flags:\n\n")
|
||||||
|
renderToBuf(&buf, []kv{
|
||||||
|
{"--auth.users", user},
|
||||||
|
}, "=")
|
||||||
|
|
||||||
|
// yaml config
|
||||||
|
fmt.Fprint(&buf, "\nYAML config:\n\n")
|
||||||
|
|
||||||
|
err = renderYamlToBuf(&buf, &model.Config{
|
||||||
|
Auth: model.AuthConfig{
|
||||||
|
Users: []string{user},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString("\n")
|
||||||
|
|
||||||
|
// footer
|
||||||
|
fmt.Fprint(&buf, "Use your config option of choice to add the user to Tinyauth and then restart.")
|
||||||
|
|
||||||
|
fmt.Println(buf.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/mdp/qrterminal/v3"
|
"github.com/mdp/qrterminal/v3"
|
||||||
@@ -34,85 +33,98 @@ func generateTotpCmd() *cli.Command {
|
|||||||
&cli.FlagLoader{},
|
&cli.FlagLoader{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cli.Command{
|
cmd := &cli.Command{
|
||||||
Name: "generate",
|
Name: "generate",
|
||||||
Description: "Generate a TOTP secret",
|
Description: "Generate a TOTP secret",
|
||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
|
||||||
log := logger.NewLogger().WithSimpleConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
if tCfg.Interactive {
|
|
||||||
form := huh.NewForm(
|
|
||||||
huh.NewGroup(
|
|
||||||
huh.NewInput().Title("Current user (username:hash)").Value(&tCfg.User).Validate((func(s string) error {
|
|
||||||
if s == "" {
|
|
||||||
return errors.New("user cannot be empty")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
theme := new(themeBase)
|
|
||||||
err := form.WithTheme(theme).Run()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := utils.ParseUser(tCfg.User)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
docker := false
|
|
||||||
if strings.Contains(tCfg.User, "$$") {
|
|
||||||
docker = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.TOTPSecret != "" {
|
|
||||||
return fmt.Errorf("user already has a TOTP secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := totp.Generate(totp.GenerateOpts{
|
|
||||||
Issuer: "Tinyauth",
|
|
||||||
AccountName: user.Username,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to generate TOTP secret: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
secret := key.Secret()
|
|
||||||
|
|
||||||
log.App.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
|
||||||
|
|
||||||
log.App.Info().Msg("Generated QR code")
|
|
||||||
|
|
||||||
config := qrterminal.Config{
|
|
||||||
Level: qrterminal.L,
|
|
||||||
Writer: os.Stdout,
|
|
||||||
BlackChar: qrterminal.BLACK,
|
|
||||||
WhiteChar: qrterminal.WHITE,
|
|
||||||
QuietZone: 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
|
||||||
|
|
||||||
user.TOTPSecret = secret
|
|
||||||
|
|
||||||
// If using docker escape re-escape it
|
|
||||||
if docker {
|
|
||||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.App.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Run = func(_ []string) error {
|
||||||
|
colors := getColors()
|
||||||
|
|
||||||
|
if tCfg.Interactive {
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewInput().Title("Current user (username:hash)").Value(&tCfg.User).Validate((func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return errors.New("user cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
theme := new(themeBase)
|
||||||
|
err := form.WithTheme(theme).Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tCfg.User == "" {
|
||||||
|
cmd.PrintHelp(os.Stdout)
|
||||||
|
return fmt.Errorf("user is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := utils.ParseUser(tCfg.User)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
docker := false
|
||||||
|
if strings.Contains(tCfg.User, "$$") {
|
||||||
|
docker = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.TOTPSecret != "" {
|
||||||
|
return fmt.Errorf("user already has a TOTP secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := totp.Generate(totp.GenerateOpts{
|
||||||
|
Issuer: "Tinyauth",
|
||||||
|
AccountName: user.Username,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate TOTP secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := key.Secret()
|
||||||
|
|
||||||
|
fmt.Printf("Scan the following QR code with your authenticator app (e.g., Google Authenticator, 2fauth, Microsoft Authenticator):\n\n")
|
||||||
|
|
||||||
|
config := qrterminal.Config{
|
||||||
|
Level: qrterminal.L,
|
||||||
|
Writer: os.Stdout,
|
||||||
|
BlackChar: qrterminal.BLACK,
|
||||||
|
WhiteChar: qrterminal.WHITE,
|
||||||
|
QuietZone: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||||
|
|
||||||
|
user.TOTPSecret = secret
|
||||||
|
|
||||||
|
// If using docker escape re-escape it
|
||||||
|
if docker {
|
||||||
|
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||||
|
}
|
||||||
|
|
||||||
|
userStr := fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)
|
||||||
|
|
||||||
|
fmt.Print("\nOr add the following TOTP secret to your authenticator app: ")
|
||||||
|
fmt.Print(colors.green.Render(secret))
|
||||||
|
fmt.Print("\n\n")
|
||||||
|
|
||||||
|
fmt.Printf("Finally, add your user '%s' back to your configuration: ", user.Username)
|
||||||
|
fmt.Print(colors.green.Render(userStr))
|
||||||
|
fmt.Print("\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
+147
-16
@@ -2,18 +2,23 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/loaders"
|
"github.com/tinyauthapp/tinyauth/internal/utils/loaders"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
tConfig := model.NewDefaultConfiguration()
|
env := model.DetectRuntimeEnv()
|
||||||
|
tConfig := model.NewDefaultConfiguration(env)
|
||||||
|
|
||||||
loaders := []cli.ResourceLoader{
|
loaders := []cli.ResourceLoader{
|
||||||
&loaders.FileLoader{},
|
&loaders.FileLoader{},
|
||||||
@@ -27,83 +32,114 @@ func main() {
|
|||||||
Configuration: tConfig,
|
Configuration: tConfig,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
Run: func(_ []string) error {
|
||||||
|
if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) {
|
||||||
|
colors := getColors()
|
||||||
|
fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.")
|
||||||
|
}
|
||||||
return runCmd(*tConfig)
|
return runCmd(*tConfig)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdUser := &cli.Command{
|
cmdUser := &cli.Command{
|
||||||
Name: "user",
|
Name: "user",
|
||||||
Description: "Manage Tinyauth users",
|
Description: "Manage users",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdTotp := &cli.Command{
|
cmdTotp := &cli.Command{
|
||||||
Name: "totp",
|
Name: "totp",
|
||||||
Description: "Manage Tinyauth TOTP users",
|
Description: "Manage TOTP users",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdOidc := &cli.Command{
|
cmdOidc := &cli.Command{
|
||||||
Name: "oidc",
|
Name: "oidc",
|
||||||
Description: "Manage Tinyauth OIDC clients",
|
Description: "Manage OIDC clients",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := cmdTinyauth.AddCommand(versionCmd())
|
helpCmd := &cli.Command{
|
||||||
|
Name: "help",
|
||||||
|
Description: "Show the help message",
|
||||||
|
Run: func(_ []string) error {
|
||||||
|
return cmdTinyauth.PrintHelp(os.Stdout)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmdTinyauth.AddCommand(helpCmd)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add version command")
|
fatalf(err, "Failed to add help command")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmdTinyauth.AddCommand(versionCmd())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fatalf(err, "Failed to add version command")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmdTinyauth.AddCommand(configCmd(tConfig, loaders))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fatalf(err, "Failed to add config command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdUser.AddCommand(verifyUserCmd())
|
err = cmdUser.AddCommand(verifyUserCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add verify command")
|
fatalf(err, "Failed to add user verify command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(healthcheckCmd())
|
err = cmdTinyauth.AddCommand(healthcheckCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add healthcheck command")
|
fatalf(err, "Failed to add healthcheck command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTotp.AddCommand(generateTotpCmd())
|
err = cmdTotp.AddCommand(generateTotpCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add generate command")
|
fatalf(err, "Failed to add totp generate command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdUser.AddCommand(createUserCmd())
|
err = cmdUser.AddCommand(createUserCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
fatalf(err, "Failed to add create user command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdOidc.AddCommand(createOidcClientCmd())
|
err = cmdOidc.AddCommand(createOidcClientCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
fatalf(err, "Failed to add create oidc client command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(cmdUser)
|
err = cmdTinyauth.AddCommand(cmdUser)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add user command")
|
fatalf(err, "Failed to add user command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(cmdTotp)
|
err = cmdTinyauth.AddCommand(cmdTotp)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add totp command")
|
fatalf(err, "Failed to add totp command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(cmdOidc)
|
err = cmdTinyauth.AddCommand(cmdOidc)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add oidc command")
|
fatalf(err, "Failed to add oidc command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.Execute(cmdTinyauth)
|
err = cli.Execute(cmdTinyauth)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to execute command")
|
if strings.Contains(err.Error(), "command not found") {
|
||||||
|
fmt.Println("Command not found. Use 'tinyauth help' to see available commands.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "is not runnable") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fatalf(err, "Failed to execute command")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,3 +160,98 @@ type themeBase struct{}
|
|||||||
func (t *themeBase) Theme(isDark bool) *huh.Styles {
|
func (t *themeBase) Theme(isDark bool) *huh.Styles {
|
||||||
return huh.ThemeBase(isDark)
|
return huh.ThemeBase(isDark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type colors struct {
|
||||||
|
blue lipgloss.Style
|
||||||
|
gray lipgloss.Style
|
||||||
|
lightGray lipgloss.Style
|
||||||
|
green lipgloss.Style
|
||||||
|
yellow lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func getColors() colors {
|
||||||
|
noColor := os.Getenv("NO_COLOR")
|
||||||
|
forceColor := os.Getenv("FORCE_COLOR")
|
||||||
|
|
||||||
|
colorOut := colors{
|
||||||
|
green: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(34)),
|
||||||
|
gray: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(245)),
|
||||||
|
yellow: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(214)),
|
||||||
|
blue: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(75)),
|
||||||
|
lightGray: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(250)),
|
||||||
|
}
|
||||||
|
|
||||||
|
noColorOut := colors{
|
||||||
|
green: lipgloss.NewStyle(),
|
||||||
|
gray: lipgloss.NewStyle(),
|
||||||
|
yellow: lipgloss.NewStyle(),
|
||||||
|
blue: lipgloss.NewStyle(),
|
||||||
|
lightGray: lipgloss.NewStyle(),
|
||||||
|
}
|
||||||
|
|
||||||
|
useColors := true
|
||||||
|
|
||||||
|
if noColor == "true" || noColor == "1" {
|
||||||
|
useColors = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if forceColor == "true" || forceColor == "1" {
|
||||||
|
useColors = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !useColors {
|
||||||
|
return noColorOut
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorOut
|
||||||
|
}
|
||||||
|
|
||||||
|
func fatalf(err error, msg string) {
|
||||||
|
fmt.Printf("%s: %v\n", msg, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type kv struct {
|
||||||
|
k string
|
||||||
|
v string
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderToBuf(buf *strings.Builder, kv []kv, sep string) {
|
||||||
|
colors := getColors()
|
||||||
|
for _, i := range kv {
|
||||||
|
buf.WriteString(colors.blue.Render(i.k))
|
||||||
|
buf.WriteString(colors.gray.Render(sep))
|
||||||
|
buf.WriteString(colors.lightGray.Render(i.v))
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderYamlToBuf(buf *strings.Builder, i any) error {
|
||||||
|
colors := getColors()
|
||||||
|
|
||||||
|
yout, err := yaml.Marshal(i)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for l := range strings.SplitSeq(string(yout), "\n") {
|
||||||
|
if l == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(strings.TrimLeft(l, " "), "- ") {
|
||||||
|
buf.WriteString(colors.lightGray.Render(l))
|
||||||
|
buf.WriteString("\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lp := strings.SplitN(l, ":", 2)
|
||||||
|
buf.WriteString(colors.blue.Render(lp[0]))
|
||||||
|
buf.WriteString(colors.gray.Render(":"))
|
||||||
|
if len(lp) == 2 {
|
||||||
|
buf.WriteString(colors.lightGray.Render(lp[1]))
|
||||||
|
}
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+79
-73
@@ -3,9 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
@@ -38,81 +38,87 @@ func verifyUserCmd() *cli.Command {
|
|||||||
&cli.FlagLoader{},
|
&cli.FlagLoader{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cli.Command{
|
cmd := &cli.Command{
|
||||||
Name: "verify",
|
Name: "verify",
|
||||||
Description: "Verify a user is set up correctly",
|
Description: "Verify a user is set up correctly",
|
||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
|
||||||
log := logger.NewLogger().WithSimpleConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
if tCfg.Interactive {
|
|
||||||
form := huh.NewForm(
|
|
||||||
huh.NewGroup(
|
|
||||||
huh.NewInput().Title("User (username:hash:totp)").Value(&tCfg.User).Validate((func(s string) error {
|
|
||||||
if s == "" {
|
|
||||||
return errors.New("user cannot be empty")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})),
|
|
||||||
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
|
|
||||||
if s == "" {
|
|
||||||
return errors.New("username cannot be empty")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})),
|
|
||||||
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
|
|
||||||
if s == "" {
|
|
||||||
return errors.New("password cannot be empty")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})),
|
|
||||||
huh.NewInput().Title("TOTP Code (optional)").Value(&tCfg.Totp),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
theme := new(themeBase)
|
|
||||||
err := form.WithTheme(theme).Run()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := utils.ParseUser(tCfg.User)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.Username != tCfg.Username {
|
|
||||||
return fmt.Errorf("username is incorrect")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(tCfg.Password))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("password is incorrect: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.TOTPSecret == "" {
|
|
||||||
if tCfg.Totp != "" {
|
|
||||||
log.App.Warn().Msg("User does not have TOTP secret")
|
|
||||||
}
|
|
||||||
log.App.Info().Msg("User verified")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ok := totp.Validate(tCfg.Totp, user.TOTPSecret)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("TOTP code incorrect")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.App.Info().Msg("User verified")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Run = func(_ []string) error {
|
||||||
|
colors := getColors()
|
||||||
|
|
||||||
|
if tCfg.Interactive {
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewInput().Title("User (username:hash:totp)").Value(&tCfg.User).Validate((func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return errors.New("user cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})),
|
||||||
|
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return errors.New("username cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})),
|
||||||
|
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return errors.New("password cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})),
|
||||||
|
huh.NewInput().Title("TOTP Code (optional)").Value(&tCfg.Totp),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
theme := new(themeBase)
|
||||||
|
err := form.WithTheme(theme).Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tCfg.User == "" || tCfg.Username == "" || tCfg.Password == "" {
|
||||||
|
cmd.PrintHelp(os.Stdout)
|
||||||
|
return fmt.Errorf("user, username, and password are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := utils.ParseUser(tCfg.User)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Username != tCfg.Username {
|
||||||
|
return fmt.Errorf("username is incorrect")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(tCfg.Password))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("password is incorrect: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.TOTPSecret == "" {
|
||||||
|
if tCfg.Totp != "" {
|
||||||
|
fmt.Println(colors.yellow.Render("⚠") + " TOTP code provided but user does not have TOTP enabled")
|
||||||
|
}
|
||||||
|
fmt.Println(colors.green.Render("✓") + " User verified")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := totp.Validate(tCfg.Totp, user.TOTPSecret)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("TOTP code incorrect")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(colors.green.Render("✓") + " User verified")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ func versionCmd() *cli.Command {
|
|||||||
Configuration: nil,
|
Configuration: nil,
|
||||||
Resources: nil,
|
Resources: nil,
|
||||||
Run: func(_ []string) error {
|
Run: func(_ []string) error {
|
||||||
fmt.Printf("Version: %s\n", model.Version)
|
colors := getColors()
|
||||||
fmt.Printf("Commit Hash: %s\n", model.CommitHash)
|
fmt.Printf("Version: %s\n", colors.blue.Render(model.Version))
|
||||||
fmt.Printf("Build Timestamp: %s\n", model.BuildTimestamp)
|
fmt.Printf("Commit Hash: %s\n", colors.blue.Render(model.CommitHash))
|
||||||
|
fmt.Printf("Build Timestamp: %s\n", colors.blue.Render(model.BuildTimestamp))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.5.0",
|
"globals": "^17.5.0",
|
||||||
"prettier": "3.8.2",
|
|
||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
|
|||||||
Generated
-10
@@ -120,9 +120,6 @@ importers:
|
|||||||
globals:
|
globals:
|
||||||
specifier: ^17.5.0
|
specifier: ^17.5.0
|
||||||
version: 17.6.0
|
version: 17.6.0
|
||||||
prettier:
|
|
||||||
specifier: 3.8.2
|
|
||||||
version: 3.8.2
|
|
||||||
rollup-plugin-visualizer:
|
rollup-plugin-visualizer:
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.0.1(rolldown@1.0.1)
|
version: 7.0.1(rolldown@1.0.1)
|
||||||
@@ -2148,11 +2145,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
prettier@3.8.2:
|
|
||||||
resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
property-information@7.1.0:
|
property-information@7.1.0:
|
||||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||||
|
|
||||||
@@ -4658,8 +4650,6 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
prettier@3.8.2: {}
|
|
||||||
|
|
||||||
property-information@7.1.0: {}
|
property-information@7.1.0: {}
|
||||||
|
|
||||||
proxy-from-env@2.1.0: {}
|
proxy-from-env@2.1.0: {}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
dangerouslyAllowAllBuilds: false
|
dangerouslyAllowAllBuilds: false
|
||||||
blockExoticSubdeps: true
|
blockExoticSubdeps: true
|
||||||
minimumReleaseAge: 1440 # 1 day
|
minimumReleaseAge: 1440 # 1 day
|
||||||
trustPolicy: no-downgrade
|
|
||||||
@@ -62,6 +62,11 @@ export default defineConfig({
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/authorize/, ""),
|
rewrite: (path) => path.replace(/^\/authorize/, ""),
|
||||||
},
|
},
|
||||||
|
"/swagger": {
|
||||||
|
target: "http://tinyauth-backend:3000/swagger",
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/swagger/, ""),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type EnvEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateExampleEnv() {
|
func generateExampleEnv() {
|
||||||
cfg := model.NewDefaultConfiguration()
|
cfg := model.NewDefaultConfiguration(model.RuntimeEnvUnknown)
|
||||||
entries := make([]EnvEntry, 0)
|
entries := make([]EnvEntry, 0)
|
||||||
|
|
||||||
root := reflect.TypeOf(cfg).Elem()
|
root := reflect.TypeOf(cfg).Elem()
|
||||||
@@ -21,7 +21,7 @@ type MarkdownEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateMarkdown() {
|
func generateMarkdown() {
|
||||||
cfg := model.NewDefaultConfiguration()
|
cfg := model.NewDefaultConfiguration(model.RuntimeEnvUnknown)
|
||||||
entries := make([]MarkdownEntry, 0)
|
entries := make([]MarkdownEntry, 0)
|
||||||
|
|
||||||
root := reflect.TypeOf(cfg).Elem()
|
root := reflect.TypeOf(cfg).Elem()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// gen/sqlc-wrapper generates store.go wrapper files for each sqlc driver package under
|
// gen/sqlc_wrapper generates store.go wrapper files for each sqlc driver package under
|
||||||
// internal/repository/<driver>/. Run via:
|
// internal/repository/<driver>/. Run via:
|
||||||
//
|
//
|
||||||
// go generate ./internal/repository/...
|
// go generate ./internal/repository/...
|
||||||
@@ -32,7 +32,7 @@ import (
|
|||||||
var storeSrc string
|
var storeSrc string
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("sqlc-wrapper: generating store.go files for sqlc driver packages...")
|
fmt.Println("sqlc_wrapper: generating store.go files for sqlc driver packages...")
|
||||||
if err := run(); err != nil {
|
if err := run(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package tinyauth
|
||||||
|
|
||||||
|
//go:generate go run github.com/tinyauthapp/tinyauth/gen/docs
|
||||||
@@ -4,6 +4,7 @@ go 1.26.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/huh/v2 v2.0.3
|
charm.land/huh/v2 v2.0.3
|
||||||
|
charm.land/lipgloss/v2 v2.0.1
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3
|
github.com/cenkalti/backoff/v5 v5.0.3
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
@@ -19,12 +20,14 @@ require (
|
|||||||
github.com/rs/zerolog v1.35.1
|
github.com/rs/zerolog v1.35.1
|
||||||
github.com/steveiliop56/ding v0.2.0
|
github.com/steveiliop56/ding v0.2.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/swaggo/swag v1.16.6
|
||||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
|
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
|
||||||
github.com/weppos/publicsuffix-go v0.50.3
|
github.com/weppos/publicsuffix-go v0.50.3
|
||||||
go.uber.org/dig v1.19.0
|
go.uber.org/dig v1.19.0
|
||||||
golang.org/x/crypto v0.53.0
|
golang.org/x/crypto v0.53.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/tools v0.47.0
|
golang.org/x/tools v0.47.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
k8s.io/apimachinery v0.36.2
|
k8s.io/apimachinery v0.36.2
|
||||||
k8s.io/client-go v0.36.2
|
k8s.io/client-go v0.36.2
|
||||||
modernc.org/sqlite v1.53.0
|
modernc.org/sqlite v1.53.0
|
||||||
@@ -34,11 +37,11 @@ require (
|
|||||||
require (
|
require (
|
||||||
charm.land/bubbles/v2 v2.0.0 // indirect
|
charm.land/bubbles/v2 v2.0.0 // indirect
|
||||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||||
charm.land/lipgloss/v2 v2.0.1 // indirect
|
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
filippo.io/edwards25519 v1.2.0 // indirect
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||||
@@ -82,6 +85,10 @@ require (
|
|||||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.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-playground/validator/v10 v10.30.1 // indirect
|
||||||
@@ -98,12 +105,14 @@ require (
|
|||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.18.5 // indirect
|
github.com/klauspost/compress v1.18.5 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||||
@@ -134,6 +143,8 @@ require (
|
|||||||
github.com/safchain/ethtool v0.3.0 // indirect
|
github.com/safchain/ethtool v0.3.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/swaggo/files v1.0.1 // indirect
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1 // indirect
|
||||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
|
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
|
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
|
||||||
@@ -169,7 +180,6 @@ require (
|
|||||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
||||||
k8s.io/klog/v2 v2.140.0 // indirect
|
k8s.io/klog/v2 v2.140.0 // indirect
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDP
|
|||||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
@@ -30,6 +32,8 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe
|
|||||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||||
@@ -133,6 +137,7 @@ github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7u
|
|||||||
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
|
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
|
||||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -192,10 +197,17 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||||
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
||||||
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
|
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
|
||||||
|
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||||
|
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
@@ -297,8 +309,11 @@ github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS
|
|||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
@@ -307,6 +322,9 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
@@ -361,6 +379,7 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
|
|||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
@@ -414,6 +433,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
|||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
@@ -421,6 +441,12 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||||
|
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/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=
|
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=
|
||||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||||
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
|
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
|
||||||
@@ -463,6 +489,7 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
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.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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
@@ -499,6 +526,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
|||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.22.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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
@@ -507,27 +536,54 @@ golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9
|
|||||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
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/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
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=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
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/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||||
@@ -542,13 +598,19 @@ google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07
|
|||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.runtime.ConfiguredProviders = configuredProviders
|
app.runtime.ConfiguredProviders = configuredProviders
|
||||||
|
|
||||||
// if tailscale is enabled and listening, replace the app url with the tailscale hostname
|
// if tailscale is enabled and listening, replace the app url with the tailscale hostname
|
||||||
if app.services.tailscaleService != nil && app.config.Tailscale.Listen {
|
if app.services.tailscaleService != nil && app.config.Experimental.Tailscale.Listen {
|
||||||
tailscaleUrl := "https://" + app.services.tailscaleService.GetHostname()
|
tailscaleUrl := "https://" + app.services.tailscaleService.GetHostname()
|
||||||
|
|
||||||
// if the tailscale url is different from the app url, replace it
|
// if the tailscale url is different from the app url, replace it
|
||||||
|
|||||||
@@ -6,17 +6,28 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
swaggerfiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
docs "github.com/tinyauthapp/tinyauth/internal/swagger"
|
||||||
"go.uber.org/dig"
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @title Tinyauth API
|
||||||
|
// @version development
|
||||||
|
// @description Swagger documentation for Tinyauth's API.
|
||||||
|
// @license.name AGPL-3.0
|
||||||
|
// @license.url https://github.com/tinyauthapp/tinyauth/blob/main/LICENSE
|
||||||
|
// @BasePath /
|
||||||
func (app *BootstrapApp) setupRouter() error {
|
func (app *BootstrapApp) setupRouter() error {
|
||||||
// we don't want gin debug mode
|
// we don't want gin debug mode
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
@@ -80,6 +91,12 @@ func (app *BootstrapApp) setupRouter() error {
|
|||||||
return fmt.Errorf("failed to provide api router group: %w", err)
|
return fmt.Errorf("failed to provide api router group: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = app.setupSwagger()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to setup swagger: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
controllerProvideFor := []any{
|
controllerProvideFor := []any{
|
||||||
controller.NewContextController,
|
controller.NewContextController,
|
||||||
controller.NewOAuthController,
|
controller.NewOAuthController,
|
||||||
@@ -125,14 +142,50 @@ func (app *BootstrapApp) setupRouter() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *BootstrapApp) setupSwagger() error {
|
||||||
|
appUrl, err := url.Parse(app.runtime.AppURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse app url: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
docs.SwaggerInfo.Host = appUrl.Host
|
||||||
|
docs.SwaggerInfo.Schemes = []string{appUrl.Scheme}
|
||||||
|
docs.SwaggerInfo.Version = model.Version
|
||||||
|
|
||||||
|
type swaggerInput struct {
|
||||||
|
dig.In
|
||||||
|
|
||||||
|
RouterGroup *gin.RouterGroup `name:"mainRouterGroup"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.dig.Invoke(func(i swaggerInput) {
|
||||||
|
i.RouterGroup.Use(func(c *gin.Context) {
|
||||||
|
if strings.TrimSuffix(c.Request.URL.Path, "/") == "/swagger" {
|
||||||
|
c.Redirect(http.StatusFound, "/swagger/index.html")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
i.RouterGroup.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to invoke swagger: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Top down
|
// Top down
|
||||||
// 1. Tailscale (if tailscale.listen)
|
// 1. Tailscale (if tailscale.listen)
|
||||||
// 2. Unix socket (if server.socketPath)
|
// 2. Unix socket (if server.socketPath)
|
||||||
// 3. HTTP - default
|
// 3. HTTP - default
|
||||||
func (app *BootstrapApp) getListenerFunc() (func(ctx context.Context) error, error) {
|
func (app *BootstrapApp) getListenerFunc() (func(ctx context.Context) error, error) {
|
||||||
if app.config.Tailscale.Listen {
|
if app.config.Experimental.Tailscale.Listen {
|
||||||
if app.services.tailscaleService == nil {
|
if app.services.tailscaleService == nil {
|
||||||
return nil, fmt.Errorf("tailscale.listen is enabled but tailscale service is not initialized")
|
return nil, fmt.Errorf("experimental.tailscale.listen is enabled but tailscale service is not initialized")
|
||||||
}
|
}
|
||||||
return app.serveTailscale, nil
|
return app.serveTailscale, nil
|
||||||
}
|
}
|
||||||
@@ -227,7 +280,6 @@ func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, ctx c
|
|||||||
err := server.Serve(listener)
|
err := server.Serve(listener)
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
shutdown()
|
|
||||||
return fmt.Errorf("failed to start %s listener: %w", name, err)
|
return fmt.Errorf("failed to start %s listener: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ func NewContextController(i ContextControllerInput) *ContextController {
|
|||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserContext godoc
|
||||||
|
//
|
||||||
|
// @Summary User context
|
||||||
|
// @Description Get the user context
|
||||||
|
// @Tags context
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} UserContextResponse
|
||||||
|
// @Router /api/context/user [get]
|
||||||
func (controller *ContextController) userContextHandler(c *gin.Context) {
|
func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||||
context, err := new(model.UserContext).NewFromGin(c)
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
@@ -147,6 +155,14 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
c.JSON(200, userContext)
|
c.JSON(200, userContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppContext godoc
|
||||||
|
//
|
||||||
|
// @Summary App context
|
||||||
|
// @Description Get the app context
|
||||||
|
// @Tags context
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} AppContextResponse
|
||||||
|
// @Router /api/context/app [get]
|
||||||
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
||||||
c.JSON(200, AppContextResponse{
|
c.JSON(200, AppContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const (
|
|||||||
FrontendLoginForApp FrontendLoginFor = "app"
|
FrontendLoginForApp FrontendLoginFor = "app"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SimpleResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
type UnauthorizedQuery struct {
|
type UnauthorizedQuery struct {
|
||||||
Username string `url:"username"`
|
Username string `url:"username"`
|
||||||
Resource string `url:"resource"`
|
Resource string `url:"resource"`
|
||||||
|
|||||||
@@ -23,9 +23,18 @@ func NewHealthController(i HealthControllerInput) *HealthController {
|
|||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HealthCheck godoc
|
||||||
|
//
|
||||||
|
// @Summary Healthcheck
|
||||||
|
// @Description Check if the server is up and running
|
||||||
|
// @Tags health
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} SimpleResponse
|
||||||
|
// @Router /api/healthz [get]
|
||||||
|
// @Router /api/healthz [head]
|
||||||
func (controller *HealthController) healthHandler(c *gin.Context) {
|
func (controller *HealthController) healthHandler(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Healthy",
|
Message: "OK",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,27 @@ func NewOAuthController(i OAuthControllerInput) *OAuthController {
|
|||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OAuthURLSuccessResponse struct {
|
||||||
|
SimpleResponse
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthURL godoc
|
||||||
|
//
|
||||||
|
// @Summary OAuth URL
|
||||||
|
// @Description Get an OAuth URL for the specified provider
|
||||||
|
// @Tags oauth
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Provider ID"
|
||||||
|
// @Param login_for query string false "Login for"
|
||||||
|
// @Param oidc_ticket query string false "OpenID Connect Ticket"
|
||||||
|
// @Param oidc_scope query string false "OpenID Connect Scope"
|
||||||
|
// @Param oidc_name query string false "OpenID Connect Name"
|
||||||
|
// @Param redirect_uri query string false "Redirect URI"
|
||||||
|
// @Success 200 {object} OAuthURLSuccessResponse
|
||||||
|
// @Failure 400 {object} SimpleResponse
|
||||||
|
// @Failure 500 {object} SimpleResponse
|
||||||
|
// @Router /api/oauth/url/{id} [get]
|
||||||
func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||||
var req OAuthRequest
|
var req OAuthRequest
|
||||||
|
|
||||||
@@ -111,23 +132,33 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
|
|
||||||
c.SetCookie(controller.runtime.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.Auth.SecureCookie, true)
|
c.SetCookie(controller.runtime.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.Auth.SecureCookie, true)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, OAuthURLSuccessResponse{
|
||||||
"status": 200,
|
SimpleResponse: SimpleResponse{
|
||||||
"message": "OK",
|
Status: 200,
|
||||||
"url": authUrl,
|
Message: "OK",
|
||||||
|
},
|
||||||
|
URL: authUrl,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OAuthCallback godoc
|
||||||
|
//
|
||||||
|
// @Summary OAuth Callback
|
||||||
|
// @Description Callback URL for OAuth providers
|
||||||
|
// @Tags oauth
|
||||||
|
// @Param id path string true "Provider ID"
|
||||||
|
// @Param code query string true "State"
|
||||||
|
// @Param state query string true "Code"
|
||||||
|
// @Success 302
|
||||||
|
// @Failure 302
|
||||||
|
// @Router /api/oauth/callback/{id} [get]
|
||||||
func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||||
var req OAuthRequest
|
var req OAuthRequest
|
||||||
|
|
||||||
err := c.BindUri(&req)
|
err := c.BindUri(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to bind URI")
|
controller.log.App.Error().Err(err).Msg("Failed to get provider ID")
|
||||||
c.JSON(400, gin.H{
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
"status": 400,
|
|
||||||
"message": "Bad Request",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +166,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get OAuth session cookie")
|
controller.log.App.Error().Err(err).Msg("Failed to get OAuth session cookie")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +176,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get pending OAuth session")
|
controller.log.App.Error().Err(err).Msg("Failed to get pending OAuth session")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +185,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
state := c.Query("state")
|
state := c.Query("state")
|
||||||
if state != oauthPendingSession.State {
|
if state != oauthPendingSession.State {
|
||||||
controller.log.App.Warn().Msg("OAuth state mismatch")
|
controller.log.App.Warn().Msg("OAuth state mismatch")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +194,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to exchange code for token")
|
controller.log.App.Error().Err(err).Msg("Failed to exchange code for token")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,19 +202,19 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get user info from OAuth provider")
|
controller.log.App.Error().Err(err).Msg("Failed to get user info from OAuth provider")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
controller.log.App.Warn().Msg("OAuth provider did not return user info")
|
controller.log.App.Warn().Msg("OAuth provider did not return user info")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Email == "" {
|
if user.Email == "" {
|
||||||
controller.log.App.Warn().Msg("OAuth provider did not return an email")
|
controller.log.App.Warn().Msg("OAuth provider did not return an email")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,13 +222,13 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get OAuth service for session")
|
controller.log.App.Error().Err(err).Msg("Failed to get OAuth service for session")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if svc.ID() != req.Provider {
|
if svc.ID() != req.Provider {
|
||||||
controller.log.App.Warn().Msgf("OAuth provider mismatch: expected %s, got %s", req.Provider, svc.ID())
|
controller.log.App.Warn().Msgf("OAuth provider mismatch: expected %s, got %s", req.Provider, svc.ID())
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,11 +242,11 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
controller.log.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.runtime.AppURL, queries.Encode()))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/unauthorized?%s", controller.runtime.AppURL, queries.Encode()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +291,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to create session cookie")
|
controller.log.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,10 +304,10 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
queries, err := query.Values(oauthPendingSession.CallbackParams)
|
queries, err := query.Values(oauthPendingSession.CallbackParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to encode OIDC callback query")
|
controller.log.App.Error().Err(err).Msg("Failed to encode OIDC callback query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode()))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,15 +319,15 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to encode redirect query")
|
controller.log.App.Error().Err(err).Msg("Failed to encode redirect query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.runtime.AppURL, queries.Encode()))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/continue?%s", controller.runtime.AppURL, queries.Encode()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, controller.runtime.AppURL)
|
c.Redirect(http.StatusFound, controller.runtime.AppURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackParams) bool {
|
func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackParams) bool {
|
||||||
|
|||||||
@@ -82,6 +82,15 @@ type AuthorizeCompleteRequest struct {
|
|||||||
Ticket string `json:"ticket" binding:"required"`
|
Ticket string `json:"ticket" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthorizeCompleteResponse struct {
|
||||||
|
SimpleResponse
|
||||||
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
type OIDCControllerInput struct {
|
type OIDCControllerInput struct {
|
||||||
dig.In
|
dig.In
|
||||||
|
|
||||||
@@ -114,6 +123,36 @@ func NewOIDCController(i OIDCControllerInput) *OIDCController {
|
|||||||
// This endpoint does **not** return a code, it handles param validation, ticket creation
|
// This endpoint does **not** return a code, it handles param validation, ticket creation
|
||||||
// and then redirects to the frontend to handle the consent screen. It performs no destructive
|
// and then redirects to the frontend to handle the consent screen. It performs no destructive
|
||||||
// actions (like logging out an existing session)
|
// actions (like logging out an existing session)
|
||||||
|
// Authorize godoc
|
||||||
|
//
|
||||||
|
// @Summary Authorize
|
||||||
|
// @Description OpenID Connect Authorize Endpoint
|
||||||
|
// @Accept x-www-form-urlencoded
|
||||||
|
// @Tags oidc
|
||||||
|
// @Param scope query string false "OAuth scopes (space separated, must include openid)"
|
||||||
|
// @Param response_type query string false "Response type (e.g. code)"
|
||||||
|
// @Param client_id query string false "Client ID"
|
||||||
|
// @Param redirect_uri query string false "Redirect URI"
|
||||||
|
// @Param state query string false "Opaque state value returned to the client"
|
||||||
|
// @Param nonce query string false "Nonce for ID token replay protection"
|
||||||
|
// @Param code_challenge query string false "PKCE code challenge"
|
||||||
|
// @Param code_challenge_method query string false "PKCE code challenge method (S256 or plain)"
|
||||||
|
// @Param prompt query string false "Prompt parameter (none, login, consent)"
|
||||||
|
// @Param max_age query string false "Max authentication age in seconds"
|
||||||
|
// @Param scope formData string false "OAuth scopes (space separated, must include openid)"
|
||||||
|
// @Param response_type formData string false "Response type (e.g. code)"
|
||||||
|
// @Param client_id formData string false "Client ID"
|
||||||
|
// @Param redirect_uri formData string false "Redirect URI"
|
||||||
|
// @Param state formData string false "Opaque state value returned to the client"
|
||||||
|
// @Param nonce formData string false "Nonce for ID token replay protection"
|
||||||
|
// @Param code_challenge formData string false "PKCE code challenge"
|
||||||
|
// @Param code_challenge_method formData string false "PKCE code challenge method (S256 or plain)"
|
||||||
|
// @Param prompt formData string false "Prompt parameter (none, login, consent)"
|
||||||
|
// @Param max_age formData string false "Max authentication age in seconds"
|
||||||
|
// @Success 302
|
||||||
|
// @Failure 302
|
||||||
|
// @Router /authorize [get]
|
||||||
|
// @Router /authorize [post]
|
||||||
func (controller *OIDCController) authorize(c *gin.Context) {
|
func (controller *OIDCController) authorize(c *gin.Context) {
|
||||||
if controller.oidc == nil {
|
if controller.oidc == nil {
|
||||||
controller.authorizeError(c, authorizeErrorParams{
|
controller.authorizeError(c, authorizeErrorParams{
|
||||||
@@ -261,6 +300,16 @@ func (controller *OIDCController) authorize(c *gin.Context) {
|
|||||||
|
|
||||||
// The actual **internal** endpoint that actually creates the code and session.
|
// The actual **internal** endpoint that actually creates the code and session.
|
||||||
// It is called by the frontend after the user has logged in and given consent.
|
// It is called by the frontend after the user has logged in and given consent.
|
||||||
|
// AuthorizeComplete godoc
|
||||||
|
//
|
||||||
|
// @Summary Authorize Complete
|
||||||
|
// @Description Internal endpoint for the completion of the OpenID Connect authorization flow
|
||||||
|
// @Tags oidc
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} AuthorizeCompleteResponse
|
||||||
|
// @Failure 500
|
||||||
|
// @Router /api/oidc/authorize-complete [post]
|
||||||
func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
||||||
if controller.oidc == nil {
|
if controller.oidc == nil {
|
||||||
// For this endpoint we return JSON errors since it's called
|
// For this endpoint we return JSON errors since it's called
|
||||||
@@ -361,17 +410,44 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, AuthorizeCompleteResponse{
|
||||||
"status": 200,
|
SimpleResponse: SimpleResponse{
|
||||||
"redirect_uri": fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()),
|
Status: 200,
|
||||||
|
},
|
||||||
|
RedirectURI: fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Token godoc
|
||||||
|
//
|
||||||
|
// @Summary Token
|
||||||
|
// @Description OpenID Connect Token Endpoint
|
||||||
|
// @Tags oidc
|
||||||
|
// @Accept x-www-form-urlencoded
|
||||||
|
// @Produce json
|
||||||
|
// @Param grant_type query string true "Grant type (authorization_code or refresh_token)"
|
||||||
|
// @Param code query string false "Authorization code (required for authorization_code grant)"
|
||||||
|
// @Param redirect_uri query string false "Redirect URI (must match the one from the authorize request)"
|
||||||
|
// @Param refresh_token query string false "Refresh token (required for refresh_token grant)"
|
||||||
|
// @Param client_id query string false "Client ID (required if not using Basic auth)"
|
||||||
|
// @Param client_secret query string false "Client secret (required for confidential clients without Basic auth)"
|
||||||
|
// @Param code_verifier query string false "PKCE code verifier (required if code_challenge was sent)"
|
||||||
|
// @Param grant_type formData string false "Grant type (authorization_code or refresh_token)"
|
||||||
|
// @Param code formData string false "Authorization code (required for authorization_code grant)"
|
||||||
|
// @Param redirect_uri formData string false "Redirect URI (must match the one from the authorize request)"
|
||||||
|
// @Param refresh_token formData string false "Refresh token (required for refresh_token grant)"
|
||||||
|
// @Param client_id formData string false "Client ID (required if not using Basic auth)"
|
||||||
|
// @Param client_secret formData string false "Client secret (required for confidential clients without Basic auth)"
|
||||||
|
// @Param code_verifier formData string false "PKCE code verifier (required if code_challenge was sent)"
|
||||||
|
// @Success 200 {object} service.TokenResponse
|
||||||
|
// @Failure 400 {object} OIDCErrorResponse
|
||||||
|
// @Failure 500 {object} OIDCErrorResponse
|
||||||
|
// @Router /oidc/token [post]
|
||||||
func (controller *OIDCController) Token(c *gin.Context) {
|
func (controller *OIDCController) Token(c *gin.Context) {
|
||||||
if controller.oidc == nil {
|
if controller.oidc == nil {
|
||||||
controller.log.App.Warn().Msg("Received OIDC request but OIDC server is not configured")
|
controller.log.App.Warn().Msg("Received OIDC request but OIDC server is not configured")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -381,8 +457,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
err := c.Bind(&req)
|
err := c.Bind(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Warn().Err(err).Msg("Failed to bind token request")
|
controller.log.App.Warn().Err(err).Msg("Failed to bind token request")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -390,8 +466,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
err = controller.oidc.ValidateGrantType(req.GrantType)
|
err = controller.oidc.ValidateGrantType(req.GrantType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Warn().Err(err).Msg("Invalid grant type")
|
controller.log.App.Warn().Err(err).Msg("Invalid grant type")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -411,8 +487,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
controller.log.App.Warn().Msg("Client credentials not found in basic auth")
|
controller.log.App.Warn().Msg("Client credentials not found in basic auth")
|
||||||
c.Header("www-authenticate", `Basic realm="Tinyauth OIDC Token Endpoint"`)
|
c.Header("www-authenticate", `Basic realm="Tinyauth OIDC Token Endpoint"`)
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_client",
|
Error: "invalid_client",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -427,16 +503,16 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Client not found")
|
controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Client not found")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_client",
|
Error: "invalid_client",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.ClientSecret != creds.ClientSecret {
|
if client.ClientSecret != creds.ClientSecret {
|
||||||
controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Invalid client secret")
|
controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Invalid client secret")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_client",
|
Error: "invalid_client",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -457,15 +533,15 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to delete session for reused code")
|
controller.log.App.Error().Err(err).Msg("Failed to delete session for reused code")
|
||||||
}
|
}
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.log.App.Warn().Msg("Code not found")
|
controller.log.App.Warn().Msg("Code not found")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -475,8 +551,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
|
|
||||||
if entry.RedirectURI != req.RedirectURI {
|
if entry.RedirectURI != req.RedirectURI {
|
||||||
controller.log.App.Warn().Msg("Redirect URI does not match")
|
controller.log.App.Warn().Msg("Redirect URI does not match")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -485,8 +561,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
controller.log.App.Warn().Msg("PKCE validation failed")
|
controller.log.App.Warn().Msg("PKCE validation failed")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -495,8 +571,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to generate access token")
|
controller.log.App.Error().Err(err).Msg("Failed to generate access token")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -508,23 +584,23 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrTokenExpired) {
|
if errors.Is(err, service.ErrTokenExpired) {
|
||||||
controller.log.App.Warn().Msg("Refresh token expired")
|
controller.log.App.Warn().Msg("Refresh token expired")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, service.ErrInvalidClient) {
|
if errors.Is(err, service.ErrInvalidClient) {
|
||||||
controller.log.App.Warn().Msg("Refresh token does not belong to client")
|
controller.log.App.Warn().Msg("Refresh token does not belong to client")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to refresh access token")
|
controller.log.App.Error().Err(err).Msg("Failed to refresh access token")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -538,11 +614,25 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
c.JSON(200, tokenResponse)
|
c.JSON(200, tokenResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Userinfo godoc
|
||||||
|
//
|
||||||
|
// @Summary Userinfo
|
||||||
|
// @Description OpenID Connect Userinfo Endpoint
|
||||||
|
// @Accept x-www-form-urlencoded
|
||||||
|
// @Tags oidc
|
||||||
|
// @Param access_token formData string false "OpenID Connect Access Token"
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} service.UserinfoResponse
|
||||||
|
// @Failure 400 {object} OIDCErrorResponse
|
||||||
|
// @Failure 401 {object} OIDCErrorResponse
|
||||||
|
// @Failure 500 {object} OIDCErrorResponse
|
||||||
|
// @Router /oidc/userinfo [get]
|
||||||
|
// @Router /oidc/userinfo [post]
|
||||||
func (controller *OIDCController) Userinfo(c *gin.Context) {
|
func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||||
if controller.oidc == nil {
|
if controller.oidc == nil {
|
||||||
controller.log.App.Warn().Msg("Received OIDC userinfo request but OIDC server is not configured")
|
controller.log.App.Warn().Msg("Received OIDC userinfo request but OIDC server is not configured")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -554,16 +644,16 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
tokenType, bearerToken, ok := strings.Cut(authorization, " ")
|
tokenType, bearerToken, ok := strings.Cut(authorization, " ")
|
||||||
if !ok {
|
if !ok {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid authorization header")
|
controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid authorization header")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.ToLower(tokenType) != "bearer" {
|
if strings.ToLower(tokenType) != "bearer" {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with non-bearer token")
|
controller.log.App.Warn().Msg("OIDC userinfo accessed with non-bearer token")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -572,23 +662,23 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
} else if c.Request.Method == http.MethodPost {
|
} else if c.Request.Method == http.MethodPost {
|
||||||
if c.ContentType() != "application/x-www-form-urlencoded" {
|
if c.ContentType() != "application/x-www-form-urlencoded" {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type")
|
controller.log.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
token = c.PostForm("access_token")
|
token = c.PostForm("access_token")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo POST accessed without access_token")
|
controller.log.App.Warn().Msg("OIDC userinfo POST accessed without access_token")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo accessed without authorization header or POST body")
|
controller.log.App.Warn().Msg("OIDC userinfo accessed without authorization header or POST body")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -598,15 +688,15 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrTokenNotFound) {
|
if errors.Is(err, service.ErrTokenNotFound) {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get access token")
|
controller.log.App.Error().Err(err).Msg("Failed to get access token")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -614,8 +704,8 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
// If we don't have the openid scope, return an error
|
// If we don't have the openid scope, return an error
|
||||||
if !slices.Contains(strings.Split(entry.Scope, " "), "openid") {
|
if !slices.Contains(strings.Split(entry.Scope, " "), "openid") {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with missing openid scope")
|
controller.log.App.Warn().Msg("OIDC userinfo accessed with missing openid scope")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_scope",
|
Error: "invalid_scope",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -626,8 +716,8 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get user info")
|
controller.log.App.Error().Err(err).Msg("Failed to get user info")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -662,9 +752,11 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz
|
|||||||
redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode())
|
redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode())
|
||||||
|
|
||||||
if params.json {
|
if params.json {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, AuthorizeCompleteResponse{
|
||||||
"status": 200,
|
SimpleResponse: SimpleResponse{
|
||||||
"redirect_uri": redirectUrl,
|
Status: 200,
|
||||||
|
},
|
||||||
|
RedirectURI: redirectUrl,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -694,9 +786,11 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz
|
|||||||
}
|
}
|
||||||
|
|
||||||
if params.json {
|
if params.json {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, AuthorizeCompleteResponse{
|
||||||
"status": 200,
|
SimpleResponse: SimpleResponse{
|
||||||
"redirect_uri": redirectUrl,
|
Status: 200,
|
||||||
|
},
|
||||||
|
RedirectURI: redirectUrl,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||||
|
|
||||||
switch strings.SplitN(path, "/", 2)[0] {
|
switch strings.SplitN(path, "/", 2)[0] {
|
||||||
case "api", "resources", ".well-known", "authorize":
|
case "api", "resources", ".well-known", "authorize", "swagger":
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
case "robots.txt":
|
case "robots.txt":
|
||||||
|
|||||||
+176
-144
@@ -1,8 +1,27 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
type RuntimeEnv int
|
||||||
|
|
||||||
|
const (
|
||||||
|
RuntimeEnvUnknown RuntimeEnv = iota
|
||||||
|
RuntimeEnvDocker
|
||||||
|
)
|
||||||
|
|
||||||
|
func DetectRuntimeEnv() RuntimeEnv {
|
||||||
|
env := os.Getenv("RUNTIME_ENV")
|
||||||
|
switch env {
|
||||||
|
case "docker":
|
||||||
|
return RuntimeEnvDocker
|
||||||
|
default:
|
||||||
|
return RuntimeEnvUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
func NewDefaultConfiguration() *Config {
|
func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
|
||||||
return &Config{
|
cfg := &Config{
|
||||||
Database: DatabaseConfig{
|
Database: DatabaseConfig{
|
||||||
Driver: "sqlite",
|
Driver: "sqlite",
|
||||||
Path: "./tinyauth.db",
|
Path: "./tinyauth.db",
|
||||||
@@ -62,246 +81,259 @@ func NewDefaultConfiguration() *Config {
|
|||||||
PrivateKeyPath: "./tinyauth_oidc_key",
|
PrivateKeyPath: "./tinyauth_oidc_key",
|
||||||
PublicKeyPath: "./tinyauth_oidc_key.pub",
|
PublicKeyPath: "./tinyauth_oidc_key.pub",
|
||||||
},
|
},
|
||||||
Tailscale: TailscaleConfig{
|
Experimental: ExperimentalConfig{
|
||||||
Dir: "./tailscale_state",
|
Tailscale: TailscaleConfig{
|
||||||
|
Dir: "./tailscale_state",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
LabelProvider: "auto",
|
LabelProvider: "auto",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply path overrides for docker runtime
|
||||||
|
if runtimeEnv == RuntimeEnvDocker {
|
||||||
|
cfg.Database.Path = "/data/tinyauth.db"
|
||||||
|
cfg.Resources.Path = "/data/resources"
|
||||||
|
cfg.OIDC.PrivateKeyPath = "/data/oidc/key.pem"
|
||||||
|
cfg.OIDC.PublicKeyPath = "/data/oidc/key.pub"
|
||||||
|
cfg.Experimental.Tailscale.Dir = "/data/tailscale"
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"`
|
||||||
Database DatabaseConfig `description:"Database configuration." yaml:"database"`
|
Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"`
|
||||||
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics"`
|
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"`
|
||||||
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources"`
|
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"`
|
||||||
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
Server ServerConfig `description:"Server configuration." yaml:"server,omitempty"`
|
||||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
Auth AuthConfig `description:"Authentication configuration." yaml:"auth,omitempty"`
|
||||||
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps,omitempty"`
|
||||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth,omitempty"`
|
||||||
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc,omitempty"`
|
||||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
UI UIConfig `description:"UI customization." yaml:"ui,omitempty"`
|
||||||
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
|
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"`
|
||||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"`
|
||||||
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider"`
|
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"`
|
||||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"`
|
||||||
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"`
|
|
||||||
ConfigFile string `description:"Path to config file." yaml:"-"`
|
ConfigFile string `description:"Path to config file." yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
Driver string `description:"The database driver to use. Valid values: sqlite, postgres, memory." yaml:"driver"`
|
Driver string `description:"The database driver to use. Valid values: sqlite, postgres, memory." yaml:"driver,omitempty"`
|
||||||
Path string `description:"The path to the SQLite database file, or connection URL when driver is postgres." yaml:"path"`
|
Path string `description:"The path to the SQLite database file, or connection URL when driver is postgres." yaml:"path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnalyticsConfig struct {
|
type AnalyticsConfig struct {
|
||||||
Enabled bool `description:"Enable periodic version information collection." yaml:"enabled"`
|
Enabled bool `description:"Enable periodic version information collection." yaml:"enabled,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourcesConfig struct {
|
type ResourcesConfig struct {
|
||||||
Enabled bool `description:"Enable the resources server." yaml:"enabled"`
|
Enabled bool `description:"Enable the resources server." yaml:"enabled,omitempty"`
|
||||||
Path string `description:"The directory where resources are stored." yaml:"path"`
|
Path string `description:"The directory where resources are stored." yaml:"path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `description:"The port on which the server listens." yaml:"port"`
|
Port int `description:"The port on which the server listens." yaml:"port,omitempty"`
|
||||||
Address string `description:"The address on which the server listens." yaml:"address"`
|
Address string `description:"The address on which the server listens." yaml:"address,omitempty"`
|
||||||
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
|
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
IP IPConfig `description:"IP whitelisting config options." yaml:"ip,omitempty"`
|
||||||
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users,omitempty"`
|
||||||
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled"`
|
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled,omitempty"`
|
||||||
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"`
|
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes,omitempty"`
|
||||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
UsersFile string `description:"Path to the users file." yaml:"usersFile,omitempty"`
|
||||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie,omitempty"`
|
||||||
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry,omitempty"`
|
||||||
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"`
|
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime,omitempty"`
|
||||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout,omitempty"`
|
||||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries,omitempty"`
|
||||||
LockdownEnabled bool `description:"Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically." yaml:"lockdownEnabled"`
|
LockdownEnabled bool `description:"Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically." yaml:"lockdownEnabled,omitempty"`
|
||||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies,omitempty"`
|
||||||
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls"`
|
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserAttributes struct {
|
type UserAttributes struct {
|
||||||
Name string `description:"Full name of the user." yaml:"name"`
|
Name string `description:"Full name of the user." yaml:"name,omitempty"`
|
||||||
GivenName string `description:"Given (first) name of the user." yaml:"givenName"`
|
GivenName string `description:"Given (first) name of the user." yaml:"givenName,omitempty"`
|
||||||
FamilyName string `description:"Family (last) name of the user." yaml:"familyName"`
|
FamilyName string `description:"Family (last) name of the user." yaml:"familyName,omitempty"`
|
||||||
MiddleName string `description:"Middle name of the user." yaml:"middleName"`
|
MiddleName string `description:"Middle name of the user." yaml:"middleName,omitempty"`
|
||||||
Nickname string `description:"Nickname of the user." yaml:"nickname"`
|
Nickname string `description:"Nickname of the user." yaml:"nickname,omitempty"`
|
||||||
Profile string `description:"URL of the user's profile page." yaml:"profile"`
|
Profile string `description:"URL of the user's profile page." yaml:"profile,omitempty"`
|
||||||
Picture string `description:"URL of the user's profile picture." yaml:"picture"`
|
Picture string `description:"URL of the user's profile picture." yaml:"picture,omitempty"`
|
||||||
Website string `description:"URL of the user's website." yaml:"website"`
|
Website string `description:"URL of the user's website." yaml:"website,omitempty"`
|
||||||
Email string `description:"Email address of the user." yaml:"email"`
|
Email string `description:"Email address of the user." yaml:"email,omitempty"`
|
||||||
Gender string `description:"Gender of the user." yaml:"gender"`
|
Gender string `description:"Gender of the user." yaml:"gender,omitempty"`
|
||||||
Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate"`
|
Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate,omitempty"`
|
||||||
Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo"`
|
Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo,omitempty"`
|
||||||
Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale"`
|
Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale,omitempty"`
|
||||||
PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber"`
|
PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber,omitempty"`
|
||||||
Address AddressClaim `description:"Address of the user." yaml:"address"`
|
Address AddressClaim `description:"Address of the user." yaml:"address,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddressClaim struct {
|
type AddressClaim struct {
|
||||||
Formatted string `description:"Full mailing address, formatted for display." yaml:"formatted" json:"formatted,omitempty"`
|
Formatted string `description:"Full mailing address, formatted for display." yaml:"formatted,omitempty" json:"formatted,omitempty"`
|
||||||
StreetAddress string `description:"Street address." yaml:"streetAddress" json:"street_address,omitempty"`
|
StreetAddress string `description:"Street address." yaml:"streetAddress,omitempty" json:"street_address,omitempty"`
|
||||||
Locality string `description:"City or locality." yaml:"locality" json:"locality,omitempty"`
|
Locality string `description:"City or locality." yaml:"locality,omitempty" json:"locality,omitempty"`
|
||||||
Region string `description:"State, province, or region." yaml:"region" json:"region,omitempty"`
|
Region string `description:"State, province, or region." yaml:"region,omitempty" json:"region,omitempty"`
|
||||||
PostalCode string `description:"Zip or postal code." yaml:"postalCode" json:"postal_code,omitempty"`
|
PostalCode string `description:"Zip or postal code." yaml:"postalCode,omitempty" json:"postal_code,omitempty"`
|
||||||
Country string `description:"Country." yaml:"country" json:"country,omitempty"`
|
Country string `description:"Country." yaml:"country,omitempty" json:"country,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IPConfig struct {
|
type IPConfig struct {
|
||||||
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow,omitempty"`
|
||||||
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block,omitempty"`
|
||||||
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication entirely." yaml:"bypass"`
|
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication entirely." yaml:"bypass,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthConfig struct {
|
type OAuthConfig struct {
|
||||||
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist,omitempty"`
|
||||||
WhitelistFile string `description:"Path to the OAuth whitelist file." yaml:"whitelistFile"`
|
WhitelistFile string `description:"Path to the OAuth whitelist file." yaml:"whitelistFile,omitempty"`
|
||||||
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
|
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect,omitempty"`
|
||||||
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OIDCConfig struct {
|
type OIDCConfig struct {
|
||||||
PrivateKeyPath string `description:"Path to the private key file, including file name." yaml:"privateKeyPath"`
|
PrivateKeyPath string `description:"Path to the private key file, including file name." yaml:"privateKeyPath,omitempty"`
|
||||||
PublicKeyPath string `description:"Path to the public key file, including file name." yaml:"publicKeyPath"`
|
PublicKeyPath string `description:"Path to the public key file, including file name." yaml:"publicKeyPath,omitempty"`
|
||||||
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients"`
|
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UIConfig struct {
|
type UIConfig struct {
|
||||||
Title string `description:"The title of the UI." yaml:"title"`
|
Title string `description:"The title of the UI." yaml:"title,omitempty"`
|
||||||
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"`
|
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage,omitempty"`
|
||||||
BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage"`
|
BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage,omitempty"`
|
||||||
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled"`
|
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LDAPConfig struct {
|
type LDAPConfig struct {
|
||||||
Address string `description:"LDAP server address." yaml:"address"`
|
Address string `description:"LDAP server address." yaml:"address,omitempty"`
|
||||||
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn,omitempty"`
|
||||||
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword,omitempty"`
|
||||||
BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile"`
|
BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile,omitempty"`
|
||||||
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn,omitempty"`
|
||||||
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure,omitempty"`
|
||||||
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter,omitempty"`
|
||||||
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
|
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert,omitempty"`
|
||||||
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey,omitempty"`
|
||||||
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL"`
|
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level"`
|
Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level,omitempty"`
|
||||||
Json bool `description:"Enable JSON formatted logs." yaml:"json"`
|
Json bool `description:"Enable JSON formatted logs." yaml:"json,omitempty"`
|
||||||
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams"`
|
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogStreams struct {
|
type LogStreams struct {
|
||||||
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http"`
|
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http,omitempty"`
|
||||||
App LogStreamConfig `description:"Application logging." yaml:"app"`
|
App LogStreamConfig `description:"Application logging." yaml:"app,omitempty"`
|
||||||
Audit LogStreamConfig `description:"Audit logging." yaml:"audit"`
|
Audit LogStreamConfig `description:"Audit logging." yaml:"audit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogStreamConfig struct {
|
type LogStreamConfig struct {
|
||||||
Enabled bool `description:"Enable this log stream." yaml:"enabled"`
|
Enabled bool `description:"Enable this log stream." yaml:"enabled,omitempty"`
|
||||||
Level string `description:"Log level for this stream. Use global if empty." yaml:"level"`
|
Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// no experimental features
|
type ExperimentalConfig struct {
|
||||||
type ExperimentalConfig struct{}
|
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"`
|
||||||
|
}
|
||||||
|
|
||||||
type TailscaleConfig struct {
|
type TailscaleConfig struct {
|
||||||
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled"`
|
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"`
|
||||||
Dir string `description:"Tailscale state directory." yaml:"dir"`
|
Dir string `description:"Tailscale state directory." yaml:"dir,omitempty"`
|
||||||
Hostname string `description:"Tailscale hostname." yaml:"hostname"`
|
Hostname string `description:"Tailscale hostname." yaml:"hostname,omitempty"`
|
||||||
AuthKey string `description:"Tailscale auth key." yaml:"authKey"`
|
AuthKey string `description:"Tailscale auth key." yaml:"authKey,omitempty"`
|
||||||
Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral"`
|
Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral,omitempty"`
|
||||||
Funnel bool `description:"Enable Tailscale Funnel." yaml:"funnel"`
|
Funnel bool `description:"Enable Tailscale Funnel." yaml:"funnel,omitempty"`
|
||||||
Listen bool `description:"Listen on the Tailscale address instead of standard address." yaml:"listen"`
|
Listen bool `description:"Listen on the Tailscale address instead of standard address." yaml:"listen,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuth/OIDC config
|
// OAuth/OIDC config
|
||||||
|
|
||||||
type OAuthServiceConfig struct {
|
type OAuthServiceConfig struct {
|
||||||
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
ClientID string `description:"OAuth client ID." yaml:"clientId,omitempty"`
|
||||||
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret,omitempty"`
|
||||||
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile"`
|
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile,omitempty"`
|
||||||
Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist"`
|
Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist,omitempty"`
|
||||||
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile"`
|
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile,omitempty"`
|
||||||
Scopes []string `description:"OAuth scopes." yaml:"scopes"`
|
Scopes []string `description:"OAuth scopes." yaml:"scopes,omitempty"`
|
||||||
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl"`
|
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl,omitempty"`
|
||||||
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl"`
|
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl,omitempty"`
|
||||||
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl"`
|
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl,omitempty"`
|
||||||
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl"`
|
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl,omitempty"`
|
||||||
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure"`
|
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure,omitempty"`
|
||||||
Name string `description:"Provider name in UI." yaml:"name"`
|
Name string `description:"Provider name in UI." yaml:"name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OIDCClientConfig struct {
|
type OIDCClientConfig struct {
|
||||||
ID string `description:"OIDC client ID." yaml:"-"`
|
ID string `description:"OIDC client ID." yaml:"-"`
|
||||||
ClientID string `description:"OIDC client ID." yaml:"clientId"`
|
ClientID string `description:"OIDC client ID." yaml:"clientId,omitempty"`
|
||||||
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret"`
|
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret,omitempty"`
|
||||||
ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile"`
|
ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile,omitempty"`
|
||||||
TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris"`
|
TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris,omitempty"`
|
||||||
Name string `description:"Client name in UI." yaml:"name"`
|
Name string `description:"Client name in UI." yaml:"name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ACLsConfig struct {
|
type ACLsConfig struct {
|
||||||
Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy"`
|
Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACLs
|
// ACLs
|
||||||
|
|
||||||
type Apps struct {
|
type Apps struct {
|
||||||
Apps map[string]App `description:"App ACLs configuration." yaml:"apps"`
|
Apps map[string]App `description:"App ACLs configuration." yaml:"apps,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Config AppConfig `description:"App configuration." yaml:"config"`
|
Config AppConfig `description:"App configuration." yaml:"config,omitempty"`
|
||||||
Users AppUsers `description:"User access configuration." yaml:"users"`
|
Users AppUsers `description:"User access configuration." yaml:"users,omitempty"`
|
||||||
OAuth AppOAuth `description:"OAuth access configuration." yaml:"oauth"`
|
OAuth AppOAuth `description:"OAuth access configuration." yaml:"oauth,omitempty"`
|
||||||
IP AppIP `description:"IP access configuration." yaml:"ip"`
|
IP AppIP `description:"IP access configuration." yaml:"ip,omitempty"`
|
||||||
Response AppResponse `description:"Response customization." yaml:"response"`
|
Response AppResponse `description:"Response customization." yaml:"response,omitempty"`
|
||||||
Path AppPath `description:"Path access configuration." yaml:"path"`
|
Path AppPath `description:"Path access configuration." yaml:"path,omitempty"`
|
||||||
LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap"`
|
LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Domain string `description:"The domain of the app." yaml:"domain"`
|
Domain string `description:"The domain of the app." yaml:"domain,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppUsers struct {
|
type AppUsers struct {
|
||||||
Allow string `description:"Comma-separated list of allowed users." yaml:"allow"`
|
Allow string `description:"Comma-separated list of allowed users." yaml:"allow,omitempty"`
|
||||||
Block string `description:"Comma-separated list of blocked users." yaml:"block"`
|
Block string `description:"Comma-separated list of blocked users." yaml:"block,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppOAuth struct {
|
type AppOAuth struct {
|
||||||
Whitelist string `description:"Comma-separated list of allowed OAuth groups." yaml:"whitelist"`
|
Whitelist string `description:"Comma-separated list of allowed OAuth groups." yaml:"whitelist,omitempty"`
|
||||||
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups"`
|
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppLDAP struct {
|
type AppLDAP struct {
|
||||||
Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups"`
|
Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppIP struct {
|
type AppIP struct {
|
||||||
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow,omitempty"`
|
||||||
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block,omitempty"`
|
||||||
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication." yaml:"bypass"`
|
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication." yaml:"bypass,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppResponse struct {
|
type AppResponse struct {
|
||||||
Headers []string `description:"Custom headers to add to the response." yaml:"headers"`
|
Headers []string `description:"Custom headers to add to the response." yaml:"headers,omitempty"`
|
||||||
BasicAuth AppBasicAuth `description:"Basic authentication for the app." yaml:"basicAuth"`
|
BasicAuth AppBasicAuth `description:"Basic authentication for the app." yaml:"basicAuth,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppBasicAuth struct {
|
type AppBasicAuth struct {
|
||||||
Username string `description:"Basic auth username." yaml:"username"`
|
Username string `description:"Basic auth username." yaml:"username,omitempty"`
|
||||||
Password string `description:"Basic auth password." yaml:"password"`
|
Password string `description:"Basic auth password." yaml:"password,omitempty"`
|
||||||
PasswordFile string `description:"Path to the file containing the basic auth password." yaml:"passwordFile"`
|
PasswordFile string `description:"Path to the file containing the basic auth password." yaml:"passwordFile,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppPath struct {
|
type AppPath struct {
|
||||||
Allow string `description:"Comma-separated list of allowed paths." yaml:"allow"`
|
Allow string `description:"Comma-separated list of allowed paths." yaml:"allow,omitempty"`
|
||||||
Block string `description:"Comma-separated list of blocked paths." yaml:"block"`
|
Block string `description:"Comma-separated list of blocked paths." yaml:"block,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package postgres
|
package postgres
|
||||||
|
|
||||||
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/postgres
|
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc_wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/postgres
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package sqlite
|
package sqlite
|
||||||
|
|
||||||
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/sqlite
|
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc_wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/sqlite
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
|
|
||||||
type LdapService struct {
|
type LdapService struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
|
ctx context.Context
|
||||||
config *model.Config
|
config *model.Config
|
||||||
|
|
||||||
conn *ldapgo.Conn
|
conn *ldapgo.Conn
|
||||||
@@ -32,6 +33,7 @@ type LdapServiceInput struct {
|
|||||||
Log *logger.Logger
|
Log *logger.Logger
|
||||||
Config *model.Config
|
Config *model.Config
|
||||||
Ding *ding.Ding
|
Ding *ding.Ding
|
||||||
|
Ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLdapService(i LdapServiceInput) (*LdapService, error) {
|
func NewLdapService(i LdapServiceInput) (*LdapService, error) {
|
||||||
@@ -42,6 +44,7 @@ func NewLdapService(i LdapServiceInput) (*LdapService, error) {
|
|||||||
ldap := &LdapService{
|
ldap := &LdapService{
|
||||||
log: i.Log,
|
log: i.Log,
|
||||||
config: i.Config,
|
config: i.Config,
|
||||||
|
ctx: i.Ctx,
|
||||||
}
|
}
|
||||||
|
|
||||||
ldap.bindPw = utils.GetSecret(i.Config.LDAP.BindPassword, i.Config.LDAP.BindPasswordFile)
|
ldap.bindPw = utils.GetSecret(i.Config.LDAP.BindPassword, i.Config.LDAP.BindPasswordFile)
|
||||||
@@ -73,6 +76,8 @@ func NewLdapService(i LdapServiceInput) (*LdapService, error) {
|
|||||||
_, err := ldap.connect()
|
_, err := ldap.connect()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// 3s + 4.5s (3x1.5) = ~6.75-8.25s total wait time before giving up
|
||||||
|
err = ldap.reconnect(3 * time.Second)
|
||||||
return nil, fmt.Errorf("failed to connect to ldap server: %w", err)
|
return nil, fmt.Errorf("failed to connect to ldap server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +93,7 @@ func NewLdapService(i LdapServiceInput) (*LdapService, error) {
|
|||||||
err := ldap.heartbeat()
|
err := ldap.heartbeat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ldap.log.App.Warn().Err(err).Msg("LDAP connection heartbeat failed, attempting to reconnect")
|
ldap.log.App.Warn().Err(err).Msg("LDAP connection heartbeat failed, attempting to reconnect")
|
||||||
if reconnectErr := ldap.reconnect(); reconnectErr != nil {
|
if reconnectErr := ldap.reconnect(1 * time.Second); reconnectErr != nil {
|
||||||
ldap.log.App.Error().Err(reconnectErr).Msg("Failed to reconnect to LDAP server")
|
ldap.log.App.Error().Err(reconnectErr).Msg("Failed to reconnect to LDAP server")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -276,17 +281,19 @@ func (ldap *LdapService) heartbeat() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) reconnect() error {
|
func (ldap *LdapService) reconnect(interval time.Duration) error {
|
||||||
ldap.log.App.Info().Msg("Attempting to reconnect to LDAP server")
|
ldap.log.App.Info().Msg("Attempting to reconnect to LDAP server")
|
||||||
|
|
||||||
exp := backoff.NewExponentialBackOff()
|
exp := backoff.NewExponentialBackOff()
|
||||||
exp.InitialInterval = 500 * time.Millisecond
|
exp.InitialInterval = interval
|
||||||
exp.RandomizationFactor = 0.1
|
exp.RandomizationFactor = 0.1
|
||||||
exp.Multiplier = 1.5
|
exp.Multiplier = 1.5
|
||||||
exp.Reset()
|
exp.Reset()
|
||||||
|
|
||||||
operation := func() (*ldapgo.Conn, error) {
|
operation := func() (*ldapgo.Conn, error) {
|
||||||
ldap.conn.Close()
|
if ldap.conn != nil {
|
||||||
|
ldap.conn.Close()
|
||||||
|
}
|
||||||
conn, err := ldap.connect()
|
conn, err := ldap.connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -294,7 +301,7 @@ func (ldap *LdapService) reconnect() error {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := backoff.Retry(context.TODO(), operation, backoff.WithBackOff(exp), backoff.WithMaxTries(3))
|
_, err := backoff.Retry(ldap.ctx, operation, backoff.WithBackOff(exp), backoff.WithMaxTries(3))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -45,17 +45,17 @@ type TailscaleServiceInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
|
func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
|
||||||
if !i.Config.Tailscale.Enabled {
|
if !i.Config.Experimental.Tailscale.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := new(tsnet.Server)
|
srv := new(tsnet.Server)
|
||||||
|
|
||||||
// node options
|
// node options
|
||||||
srv.Dir = i.Config.Tailscale.Dir
|
srv.Dir = i.Config.Experimental.Tailscale.Dir
|
||||||
srv.Hostname = i.Config.Tailscale.Hostname
|
srv.Hostname = i.Config.Experimental.Tailscale.Hostname
|
||||||
srv.AuthKey = i.Config.Tailscale.AuthKey
|
srv.AuthKey = i.Config.Experimental.Tailscale.AuthKey
|
||||||
srv.Ephemeral = i.Config.Tailscale.Ephemeral
|
srv.Ephemeral = i.Config.Experimental.Tailscale.Ephemeral
|
||||||
|
|
||||||
// redirect logs to zerolog
|
// redirect logs to zerolog
|
||||||
srv.Logf = i.Log.App.Printf
|
srv.Logf = i.Log.App.Printf
|
||||||
@@ -94,7 +94,7 @@ func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
|
|||||||
|
|
||||||
i.Ding.Go(service.watchAndClose, ding.RingMajor)
|
i.Ding.Go(service.watchAndClose, ding.RingMajor)
|
||||||
|
|
||||||
if i.Config.Tailscale.Funnel && !i.Config.Tailscale.Listen {
|
if i.Config.Experimental.Tailscale.Funnel && !i.Config.Experimental.Tailscale.Listen {
|
||||||
service.log.App.Warn().Msg("Tailscale Funnel is enabled but listen is disabled. Funnel will not work without listen enabled.")
|
service.log.App.Warn().Msg("Tailscale Funnel is enabled but listen is disabled. Funnel will not work without listen enabled.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ func (ts *TailscaleService) CreateListener() (net.Listener, error) {
|
|||||||
return *ts.ln, nil
|
return *ts.ln, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if ts.config.Tailscale.Funnel {
|
if ts.config.Experimental.Tailscale.Funnel {
|
||||||
ln, err := ts.srv.ListenFunnel("tcp", ":443")
|
ln, err := ts.srv.ListenFunnel("tcp", ":443")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,690 @@
|
|||||||
|
basePath: /
|
||||||
|
definitions:
|
||||||
|
controller.ACRApp:
|
||||||
|
properties:
|
||||||
|
appUrl:
|
||||||
|
type: string
|
||||||
|
cookieDomain:
|
||||||
|
type: string
|
||||||
|
subdomainsEnabled:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
controller.ACRAuth:
|
||||||
|
properties:
|
||||||
|
providers:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.Provider'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
controller.ACROAuth:
|
||||||
|
properties:
|
||||||
|
autoRedirect:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
controller.ACRUI:
|
||||||
|
properties:
|
||||||
|
backgroundImage:
|
||||||
|
type: string
|
||||||
|
forgotPasswordMessage:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
warningsEnabled:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
controller.AppContextResponse:
|
||||||
|
properties:
|
||||||
|
app:
|
||||||
|
$ref: '#/definitions/controller.ACRApp'
|
||||||
|
auth:
|
||||||
|
$ref: '#/definitions/controller.ACRAuth'
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
oauth:
|
||||||
|
$ref: '#/definitions/controller.ACROAuth'
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
ui:
|
||||||
|
$ref: '#/definitions/controller.ACRUI'
|
||||||
|
type: object
|
||||||
|
controller.AuthorizeCompleteResponse:
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
redirect_uri:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
controller.OAuthURLSuccessResponse:
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
controller.OIDCErrorResponse:
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
controller.SimpleResponse:
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
controller.UCRAuth:
|
||||||
|
properties:
|
||||||
|
authenticated:
|
||||||
|
type: boolean
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
providerId:
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
controller.UCROAuth:
|
||||||
|
properties:
|
||||||
|
active:
|
||||||
|
type: boolean
|
||||||
|
displayName:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
controller.UCRTOTP:
|
||||||
|
properties:
|
||||||
|
pending:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
controller.UCRTailscale:
|
||||||
|
properties:
|
||||||
|
nodeName:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
controller.UserContextResponse:
|
||||||
|
properties:
|
||||||
|
auth:
|
||||||
|
$ref: '#/definitions/controller.UCRAuth'
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
oauth:
|
||||||
|
$ref: '#/definitions/controller.UCROAuth'
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
tailscale:
|
||||||
|
$ref: '#/definitions/controller.UCRTailscale'
|
||||||
|
totp:
|
||||||
|
$ref: '#/definitions/controller.UCRTOTP'
|
||||||
|
type: object
|
||||||
|
model.AddressClaim:
|
||||||
|
properties:
|
||||||
|
country:
|
||||||
|
type: string
|
||||||
|
formatted:
|
||||||
|
type: string
|
||||||
|
locality:
|
||||||
|
type: string
|
||||||
|
postal_code:
|
||||||
|
type: string
|
||||||
|
region:
|
||||||
|
type: string
|
||||||
|
street_address:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
model.Provider:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
oauth:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
service.TokenResponse:
|
||||||
|
properties:
|
||||||
|
access_token:
|
||||||
|
type: string
|
||||||
|
expires_in:
|
||||||
|
type: integer
|
||||||
|
id_token:
|
||||||
|
type: string
|
||||||
|
refresh_token:
|
||||||
|
type: string
|
||||||
|
scope:
|
||||||
|
type: string
|
||||||
|
token_type:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
service.UserinfoResponse:
|
||||||
|
properties:
|
||||||
|
address:
|
||||||
|
$ref: '#/definitions/model.AddressClaim'
|
||||||
|
birthdate:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
email_verified:
|
||||||
|
type: boolean
|
||||||
|
family_name:
|
||||||
|
type: string
|
||||||
|
gender:
|
||||||
|
type: string
|
||||||
|
given_name:
|
||||||
|
type: string
|
||||||
|
groups:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
locale:
|
||||||
|
type: string
|
||||||
|
middle_name:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
nickname:
|
||||||
|
type: string
|
||||||
|
phone_number:
|
||||||
|
type: string
|
||||||
|
phone_number_verified:
|
||||||
|
type: boolean
|
||||||
|
picture:
|
||||||
|
type: string
|
||||||
|
preferred_username:
|
||||||
|
type: string
|
||||||
|
profile:
|
||||||
|
type: string
|
||||||
|
sub:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: integer
|
||||||
|
website:
|
||||||
|
type: string
|
||||||
|
zoneinfo:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
info:
|
||||||
|
contact: {}
|
||||||
|
description: Swagger documentation for Tinyauth's API.
|
||||||
|
license:
|
||||||
|
name: AGPL-3.0
|
||||||
|
url: https://github.com/tinyauthapp/tinyauth/blob/main/LICENSE
|
||||||
|
title: Tinyauth API
|
||||||
|
version: development
|
||||||
|
paths:
|
||||||
|
/api/context/app:
|
||||||
|
get:
|
||||||
|
description: Get the app context
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.AppContextResponse'
|
||||||
|
summary: App context
|
||||||
|
tags:
|
||||||
|
- context
|
||||||
|
/api/context/user:
|
||||||
|
get:
|
||||||
|
description: Get the user context
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.UserContextResponse'
|
||||||
|
summary: User context
|
||||||
|
tags:
|
||||||
|
- context
|
||||||
|
/api/healthz:
|
||||||
|
get:
|
||||||
|
description: Check if the server is up and running
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.SimpleResponse'
|
||||||
|
summary: Healthcheck
|
||||||
|
tags:
|
||||||
|
- health
|
||||||
|
head:
|
||||||
|
description: Check if the server is up and running
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.SimpleResponse'
|
||||||
|
summary: Healthcheck
|
||||||
|
tags:
|
||||||
|
- health
|
||||||
|
/api/oauth/callback/{id}:
|
||||||
|
get:
|
||||||
|
description: Callback URL for OAuth providers
|
||||||
|
parameters:
|
||||||
|
- description: Provider ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: State
|
||||||
|
in: query
|
||||||
|
name: code
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Code
|
||||||
|
in: query
|
||||||
|
name: state
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"302":
|
||||||
|
description: Found
|
||||||
|
summary: OAuth Callback
|
||||||
|
tags:
|
||||||
|
- oauth
|
||||||
|
/api/oauth/url/{id}:
|
||||||
|
get:
|
||||||
|
description: Get an OAuth URL for the specified provider
|
||||||
|
parameters:
|
||||||
|
- description: Provider ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Login for
|
||||||
|
in: query
|
||||||
|
name: login_for
|
||||||
|
type: string
|
||||||
|
- description: OpenID Connect Ticket
|
||||||
|
in: query
|
||||||
|
name: oidc_ticket
|
||||||
|
type: string
|
||||||
|
- description: OpenID Connect Scope
|
||||||
|
in: query
|
||||||
|
name: oidc_scope
|
||||||
|
type: string
|
||||||
|
- description: OpenID Connect Name
|
||||||
|
in: query
|
||||||
|
name: oidc_name
|
||||||
|
type: string
|
||||||
|
- description: Redirect URI
|
||||||
|
in: query
|
||||||
|
name: redirect_uri
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.OAuthURLSuccessResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.SimpleResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.SimpleResponse'
|
||||||
|
summary: OAuth URL
|
||||||
|
tags:
|
||||||
|
- oauth
|
||||||
|
/api/oidc/authorize-complete:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Internal endpoint for the completion of the OpenID Connect authorization
|
||||||
|
flow
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.AuthorizeCompleteResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
summary: Authorize Complete
|
||||||
|
tags:
|
||||||
|
- oidc
|
||||||
|
/authorize:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
description: OpenID Connect Authorize Endpoint
|
||||||
|
parameters:
|
||||||
|
- description: OAuth scopes (space separated, must include openid)
|
||||||
|
in: query
|
||||||
|
name: scope
|
||||||
|
type: string
|
||||||
|
- description: Response type (e.g. code)
|
||||||
|
in: query
|
||||||
|
name: response_type
|
||||||
|
type: string
|
||||||
|
- description: Client ID
|
||||||
|
in: query
|
||||||
|
name: client_id
|
||||||
|
type: string
|
||||||
|
- description: Redirect URI
|
||||||
|
in: query
|
||||||
|
name: redirect_uri
|
||||||
|
type: string
|
||||||
|
- description: Opaque state value returned to the client
|
||||||
|
in: query
|
||||||
|
name: state
|
||||||
|
type: string
|
||||||
|
- description: Nonce for ID token replay protection
|
||||||
|
in: query
|
||||||
|
name: nonce
|
||||||
|
type: string
|
||||||
|
- description: PKCE code challenge
|
||||||
|
in: query
|
||||||
|
name: code_challenge
|
||||||
|
type: string
|
||||||
|
- description: PKCE code challenge method (S256 or plain)
|
||||||
|
in: query
|
||||||
|
name: code_challenge_method
|
||||||
|
type: string
|
||||||
|
- description: Prompt parameter (none, login, consent)
|
||||||
|
in: query
|
||||||
|
name: prompt
|
||||||
|
type: string
|
||||||
|
- description: Max authentication age in seconds
|
||||||
|
in: query
|
||||||
|
name: max_age
|
||||||
|
type: string
|
||||||
|
- description: OAuth scopes (space separated, must include openid)
|
||||||
|
in: formData
|
||||||
|
name: scope
|
||||||
|
type: string
|
||||||
|
- description: Response type (e.g. code)
|
||||||
|
in: formData
|
||||||
|
name: response_type
|
||||||
|
type: string
|
||||||
|
- description: Client ID
|
||||||
|
in: formData
|
||||||
|
name: client_id
|
||||||
|
type: string
|
||||||
|
- description: Redirect URI
|
||||||
|
in: formData
|
||||||
|
name: redirect_uri
|
||||||
|
type: string
|
||||||
|
- description: Opaque state value returned to the client
|
||||||
|
in: formData
|
||||||
|
name: state
|
||||||
|
type: string
|
||||||
|
- description: Nonce for ID token replay protection
|
||||||
|
in: formData
|
||||||
|
name: nonce
|
||||||
|
type: string
|
||||||
|
- description: PKCE code challenge
|
||||||
|
in: formData
|
||||||
|
name: code_challenge
|
||||||
|
type: string
|
||||||
|
- description: PKCE code challenge method (S256 or plain)
|
||||||
|
in: formData
|
||||||
|
name: code_challenge_method
|
||||||
|
type: string
|
||||||
|
- description: Prompt parameter (none, login, consent)
|
||||||
|
in: formData
|
||||||
|
name: prompt
|
||||||
|
type: string
|
||||||
|
- description: Max authentication age in seconds
|
||||||
|
in: formData
|
||||||
|
name: max_age
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"302":
|
||||||
|
description: Found
|
||||||
|
summary: Authorize
|
||||||
|
tags:
|
||||||
|
- oidc
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
description: OpenID Connect Authorize Endpoint
|
||||||
|
parameters:
|
||||||
|
- description: OAuth scopes (space separated, must include openid)
|
||||||
|
in: query
|
||||||
|
name: scope
|
||||||
|
type: string
|
||||||
|
- description: Response type (e.g. code)
|
||||||
|
in: query
|
||||||
|
name: response_type
|
||||||
|
type: string
|
||||||
|
- description: Client ID
|
||||||
|
in: query
|
||||||
|
name: client_id
|
||||||
|
type: string
|
||||||
|
- description: Redirect URI
|
||||||
|
in: query
|
||||||
|
name: redirect_uri
|
||||||
|
type: string
|
||||||
|
- description: Opaque state value returned to the client
|
||||||
|
in: query
|
||||||
|
name: state
|
||||||
|
type: string
|
||||||
|
- description: Nonce for ID token replay protection
|
||||||
|
in: query
|
||||||
|
name: nonce
|
||||||
|
type: string
|
||||||
|
- description: PKCE code challenge
|
||||||
|
in: query
|
||||||
|
name: code_challenge
|
||||||
|
type: string
|
||||||
|
- description: PKCE code challenge method (S256 or plain)
|
||||||
|
in: query
|
||||||
|
name: code_challenge_method
|
||||||
|
type: string
|
||||||
|
- description: Prompt parameter (none, login, consent)
|
||||||
|
in: query
|
||||||
|
name: prompt
|
||||||
|
type: string
|
||||||
|
- description: Max authentication age in seconds
|
||||||
|
in: query
|
||||||
|
name: max_age
|
||||||
|
type: string
|
||||||
|
- description: OAuth scopes (space separated, must include openid)
|
||||||
|
in: formData
|
||||||
|
name: scope
|
||||||
|
type: string
|
||||||
|
- description: Response type (e.g. code)
|
||||||
|
in: formData
|
||||||
|
name: response_type
|
||||||
|
type: string
|
||||||
|
- description: Client ID
|
||||||
|
in: formData
|
||||||
|
name: client_id
|
||||||
|
type: string
|
||||||
|
- description: Redirect URI
|
||||||
|
in: formData
|
||||||
|
name: redirect_uri
|
||||||
|
type: string
|
||||||
|
- description: Opaque state value returned to the client
|
||||||
|
in: formData
|
||||||
|
name: state
|
||||||
|
type: string
|
||||||
|
- description: Nonce for ID token replay protection
|
||||||
|
in: formData
|
||||||
|
name: nonce
|
||||||
|
type: string
|
||||||
|
- description: PKCE code challenge
|
||||||
|
in: formData
|
||||||
|
name: code_challenge
|
||||||
|
type: string
|
||||||
|
- description: PKCE code challenge method (S256 or plain)
|
||||||
|
in: formData
|
||||||
|
name: code_challenge_method
|
||||||
|
type: string
|
||||||
|
- description: Prompt parameter (none, login, consent)
|
||||||
|
in: formData
|
||||||
|
name: prompt
|
||||||
|
type: string
|
||||||
|
- description: Max authentication age in seconds
|
||||||
|
in: formData
|
||||||
|
name: max_age
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"302":
|
||||||
|
description: Found
|
||||||
|
summary: Authorize
|
||||||
|
tags:
|
||||||
|
- oidc
|
||||||
|
/oidc/token:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
description: OpenID Connect Token Endpoint
|
||||||
|
parameters:
|
||||||
|
- description: Grant type (authorization_code or refresh_token)
|
||||||
|
in: query
|
||||||
|
name: grant_type
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Authorization code (required for authorization_code grant)
|
||||||
|
in: query
|
||||||
|
name: code
|
||||||
|
type: string
|
||||||
|
- description: Redirect URI (must match the one from the authorize request)
|
||||||
|
in: query
|
||||||
|
name: redirect_uri
|
||||||
|
type: string
|
||||||
|
- description: Refresh token (required for refresh_token grant)
|
||||||
|
in: query
|
||||||
|
name: refresh_token
|
||||||
|
type: string
|
||||||
|
- description: Client ID (required if not using Basic auth)
|
||||||
|
in: query
|
||||||
|
name: client_id
|
||||||
|
type: string
|
||||||
|
- description: Client secret (required for confidential clients without Basic
|
||||||
|
auth)
|
||||||
|
in: query
|
||||||
|
name: client_secret
|
||||||
|
type: string
|
||||||
|
- description: PKCE code verifier (required if code_challenge was sent)
|
||||||
|
in: query
|
||||||
|
name: code_verifier
|
||||||
|
type: string
|
||||||
|
- description: Grant type (authorization_code or refresh_token)
|
||||||
|
in: formData
|
||||||
|
name: grant_type
|
||||||
|
type: string
|
||||||
|
- description: Authorization code (required for authorization_code grant)
|
||||||
|
in: formData
|
||||||
|
name: code
|
||||||
|
type: string
|
||||||
|
- description: Redirect URI (must match the one from the authorize request)
|
||||||
|
in: formData
|
||||||
|
name: redirect_uri
|
||||||
|
type: string
|
||||||
|
- description: Refresh token (required for refresh_token grant)
|
||||||
|
in: formData
|
||||||
|
name: refresh_token
|
||||||
|
type: string
|
||||||
|
- description: Client ID (required if not using Basic auth)
|
||||||
|
in: formData
|
||||||
|
name: client_id
|
||||||
|
type: string
|
||||||
|
- description: Client secret (required for confidential clients without Basic
|
||||||
|
auth)
|
||||||
|
in: formData
|
||||||
|
name: client_secret
|
||||||
|
type: string
|
||||||
|
- description: PKCE code verifier (required if code_challenge was sent)
|
||||||
|
in: formData
|
||||||
|
name: code_verifier
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/service.TokenResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.OIDCErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.OIDCErrorResponse'
|
||||||
|
summary: Token
|
||||||
|
tags:
|
||||||
|
- oidc
|
||||||
|
/oidc/userinfo:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
description: OpenID Connect Userinfo Endpoint
|
||||||
|
parameters:
|
||||||
|
- description: OpenID Connect Access Token
|
||||||
|
in: formData
|
||||||
|
name: access_token
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/service.UserinfoResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.OIDCErrorResponse'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.OIDCErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.OIDCErrorResponse'
|
||||||
|
summary: Userinfo
|
||||||
|
tags:
|
||||||
|
- oidc
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
description: OpenID Connect Userinfo Endpoint
|
||||||
|
parameters:
|
||||||
|
- description: OpenID Connect Access Token
|
||||||
|
in: formData
|
||||||
|
name: access_token
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/service.UserinfoResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.OIDCErrorResponse'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.OIDCErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/controller.OIDCErrorResponse'
|
||||||
|
summary: Userinfo
|
||||||
|
tags:
|
||||||
|
- oidc
|
||||||
|
swagger: "2.0"
|
||||||
Reference in New Issue
Block a user