mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-04 09:10:14 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fcac1b2f7 |
+17
-17
@@ -220,23 +220,6 @@ TINYAUTH_LDAP_AUTHCERT=
|
||||
TINYAUTH_LDAP_AUTHKEY=
|
||||
# Cache duration for LDAP group membership in seconds.
|
||||
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.
|
||||
TINYAUTH_LABELPROVIDER="auto"
|
||||
|
||||
@@ -258,3 +241,20 @@ TINYAUTH_LOG_STREAMS_APP_LEVEL=
|
||||
TINYAUTH_LOG_STREAMS_AUDIT_ENABLED=false
|
||||
# Log level for this stream. Use global if empty.
|
||||
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
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
sqlc generate
|
||||
go generate ./...
|
||||
git diff --exit-code
|
||||
git status --porcelain | grep -q . && echo "untracked files code gen files" && exit 1 || true
|
||||
git status --porcelain | grep -q . && echo "untracked files in git diff" && exit 1 || true
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
|
||||
@@ -51,6 +51,3 @@ config.certify.yml
|
||||
|
||||
# deepsec
|
||||
/.deepsec
|
||||
|
||||
# jetbrains
|
||||
/.idea/
|
||||
|
||||
+5
-7
@@ -52,17 +52,15 @@ WORKDIR /tinyauth
|
||||
|
||||
COPY --from=builder /tinyauth/tinyauth ./
|
||||
|
||||
EXPOSE 3000
|
||||
RUN mkdir -p /data
|
||||
|
||||
# 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
|
||||
EXPOSE 3000
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
# Tell tinyauth that it's running in a container and where to find the data directory
|
||||
ENV RUNTIME_ENV=docker
|
||||
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
|
||||
|
||||
ENV TINYAUTH_RESOURCES_PATH=/data/resources
|
||||
|
||||
ENV PATH=$PATH:/tinyauth
|
||||
|
||||
|
||||
@@ -40,16 +40,13 @@ COPY ./cmd ./cmd
|
||||
COPY ./internal ./internal
|
||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||
|
||||
RUN mkdir -p data
|
||||
|
||||
RUN CGO_ENABLED=0 go build -ldflags "${LDFLAGS} \
|
||||
-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.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
|
||||
FROM gcr.io/distroless/static-debian12:latest AS runner
|
||||
|
||||
@@ -58,14 +55,15 @@ WORKDIR /tinyauth
|
||||
COPY --from=builder /tinyauth/tinyauth ./
|
||||
|
||||
# Since it's distroless, we need to copy the data directory from the builder stage
|
||||
COPY --from=builder /data /data
|
||||
COPY --from=builder /tinyauth/data /data
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
# Tell tinyauth that it's running in a container and where to find the data directory
|
||||
ENV RUNTIME_ENV=docker
|
||||
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
|
||||
|
||||
ENV TINYAUTH_RESOURCES_PATH=/data/resources
|
||||
|
||||
ENV PATH=$PATH:/tinyauth
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c
|
||||
|
||||
.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:
|
||||
cd frontend && pnpm ci
|
||||
@@ -60,10 +58,12 @@ binary-linux-arm64:
|
||||
$(MAKE) binary
|
||||
|
||||
# Go test
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
# Go vet
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
@@ -88,25 +88,10 @@ prod-infisical:
|
||||
infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||
|
||||
# SQL
|
||||
.PHONY: sql
|
||||
sql:
|
||||
sqlc generate
|
||||
|
||||
# Go gen
|
||||
generate:
|
||||
go generate ./...
|
||||
|
||||
# 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,32 +0,0 @@
|
||||
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,9 +7,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
)
|
||||
|
||||
func createOidcClientCmd() *cli.Command {
|
||||
@@ -32,84 +31,40 @@ func createOidcClientCmd() *cli.Command {
|
||||
return errors.New("client name can only contain alphanumeric characters and hyphens")
|
||||
}
|
||||
|
||||
u := uuid.New()
|
||||
clientId := u.String()
|
||||
uuid := uuid.New()
|
||||
clientId := uuid.String()
|
||||
clientSecret := "ta-" + utils.GenerateString(61)
|
||||
|
||||
uclientName := strings.ToUpper(clientName)
|
||||
lclientName := strings.ToLower(clientName)
|
||||
|
||||
buf := strings.Builder{}
|
||||
builder := strings.Builder{}
|
||||
|
||||
// header
|
||||
fmt.Fprintf(&buf, "Created '%s' OIDC client.\n\n", clientName)
|
||||
fmt.Fprintf(&builder, "Created credentials for client %s\n\n", clientName)
|
||||
|
||||
// credentials
|
||||
fmt.Fprintf(&buf, "Credentials:\n\n")
|
||||
fmt.Fprintf(&buf, "Client Name: %s\n", clientName)
|
||||
fmt.Fprintf(&buf, "Client ID: %s\n", clientId)
|
||||
fmt.Fprintf(&buf, "Client Secret: %s\n\n", clientSecret)
|
||||
fmt.Fprintf(&builder, "Client Name: %s\n", clientName)
|
||||
fmt.Fprintf(&builder, "Client ID: %s\n", clientId)
|
||||
fmt.Fprintf(&builder, "Client Secret: %s\n\n", clientSecret)
|
||||
|
||||
// end variables
|
||||
fmt.Fprintf(&buf, "Environment variables:\n\n")
|
||||
renderToBuf(&buf, []kv{
|
||||
{
|
||||
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")
|
||||
// env variables
|
||||
fmt.Fprint(&builder, "Environment variables:\n\n")
|
||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTID=%s\n", uclientName, clientId)
|
||||
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))
|
||||
|
||||
// cli flags
|
||||
fmt.Fprintf(&buf, "CLI flags:\n\n")
|
||||
renderToBuf(&buf, []kv{
|
||||
{
|
||||
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")
|
||||
fmt.Fprint(&builder, "CLI flags:\n\n")
|
||||
fmt.Fprintf(&builder, "--oidc.clients.%s.clientid=%s\n", lclientName, clientId)
|
||||
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))
|
||||
|
||||
// footer
|
||||
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.")
|
||||
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.")
|
||||
|
||||
// print
|
||||
out := buf.String()
|
||||
out := builder.String()
|
||||
fmt.Print(out)
|
||||
return nil
|
||||
},
|
||||
|
||||
+44
-90
@@ -3,12 +3,11 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -35,107 +34,62 @@ func createUserCmd() *cli.Command {
|
||||
&cli.FlagLoader{},
|
||||
}
|
||||
|
||||
cmd := &cli.Command{
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Description: "Create a user",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
}
|
||||
Run: func(_ []string) error {
|
||||
log := logger.NewLogger().WithSimpleConfig()
|
||||
log.Init()
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
||||
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)
|
||||
theme := new(themeBase)
|
||||
err := form.WithTheme(theme).Run()
|
||||
|
||||
err := form.WithTheme(theme).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
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 tCfg.Username == "" || tCfg.Password == "" {
|
||||
return errors.New("username and password cannot be empty")
|
||||
}
|
||||
|
||||
if strings.Contains(tCfg.Username, ":") {
|
||||
return errors.New("username cannot contain ':'")
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 docker format is enabled, escape the dollar sign
|
||||
passwdStr := string(passwd)
|
||||
if tCfg.Docker {
|
||||
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||
}
|
||||
|
||||
if tCfg.Docker {
|
||||
outputStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||
}
|
||||
log.App.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
|
||||
|
||||
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 nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/mdp/qrterminal/v3"
|
||||
@@ -33,98 +34,85 @@ func generateTotpCmd() *cli.Command {
|
||||
&cli.FlagLoader{},
|
||||
}
|
||||
|
||||
cmd := &cli.Command{
|
||||
return &cli.Command{
|
||||
Name: "generate",
|
||||
Description: "Generate a TOTP secret",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
}
|
||||
Run: func(_ []string) error {
|
||||
log := logger.NewLogger().WithSimpleConfig()
|
||||
log.Init()
|
||||
|
||||
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
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
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 run interactive prompt: %w", err)
|
||||
return fmt.Errorf("failed to parse user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tCfg.User == "" {
|
||||
cmd.PrintHelp(os.Stdout)
|
||||
return fmt.Errorf("user is required")
|
||||
}
|
||||
docker := false
|
||||
if strings.Contains(tCfg.User, "$$") {
|
||||
docker = true
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(tCfg.User)
|
||||
if user.TOTPSecret != "" {
|
||||
return fmt.Errorf("user already has a TOTP secret")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse user: %w", err)
|
||||
}
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "Tinyauth",
|
||||
AccountName: user.Username,
|
||||
})
|
||||
|
||||
docker := false
|
||||
if strings.Contains(tCfg.User, "$$") {
|
||||
docker = true
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate TOTP secret: %w", err)
|
||||
}
|
||||
|
||||
if user.TOTPSecret != "" {
|
||||
return fmt.Errorf("user already has a TOTP secret")
|
||||
}
|
||||
secret := key.Secret()
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "Tinyauth",
|
||||
AccountName: user.Username,
|
||||
})
|
||||
log.App.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate TOTP secret: %w", err)
|
||||
}
|
||||
log.App.Info().Msg("Generated QR code")
|
||||
|
||||
secret := key.Secret()
|
||||
config := qrterminal.Config{
|
||||
Level: qrterminal.L,
|
||||
Writer: os.Stdout,
|
||||
BlackChar: qrterminal.BLACK,
|
||||
WhiteChar: qrterminal.WHITE,
|
||||
QuietZone: 2,
|
||||
}
|
||||
|
||||
fmt.Printf("Scan the following QR code with your authenticator app (e.g., Google Authenticator, 2fauth, Microsoft Authenticator):\n\n")
|
||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||
|
||||
config := qrterminal.Config{
|
||||
Level: qrterminal.L,
|
||||
Writer: os.Stdout,
|
||||
BlackChar: qrterminal.BLACK,
|
||||
WhiteChar: qrterminal.WHITE,
|
||||
QuietZone: 2,
|
||||
}
|
||||
user.TOTPSecret = secret
|
||||
|
||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||
// If using docker escape re-escape it
|
||||
if docker {
|
||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||
}
|
||||
|
||||
user.TOTPSecret = secret
|
||||
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.")
|
||||
|
||||
// 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 nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+16
-147
@@ -2,23 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/loaders"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
env := model.DetectRuntimeEnv()
|
||||
tConfig := model.NewDefaultConfiguration(env)
|
||||
tConfig := model.NewDefaultConfiguration()
|
||||
|
||||
loaders := []cli.ResourceLoader{
|
||||
&loaders.FileLoader{},
|
||||
@@ -32,114 +27,83 @@ func main() {
|
||||
Configuration: tConfig,
|
||||
Resources: loaders,
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
cmdUser := &cli.Command{
|
||||
Name: "user",
|
||||
Description: "Manage users",
|
||||
Description: "Manage Tinyauth users",
|
||||
}
|
||||
|
||||
cmdTotp := &cli.Command{
|
||||
Name: "totp",
|
||||
Description: "Manage TOTP users",
|
||||
Description: "Manage Tinyauth TOTP users",
|
||||
}
|
||||
|
||||
cmdOidc := &cli.Command{
|
||||
Name: "oidc",
|
||||
Description: "Manage OIDC clients",
|
||||
Description: "Manage Tinyauth OIDC clients",
|
||||
}
|
||||
|
||||
helpCmd := &cli.Command{
|
||||
Name: "help",
|
||||
Description: "Show the help message",
|
||||
Run: func(_ []string) error {
|
||||
return cmdTinyauth.PrintHelp(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
err := cmdTinyauth.AddCommand(helpCmd)
|
||||
err := cmdTinyauth.AddCommand(versionCmd())
|
||||
|
||||
if err != nil {
|
||||
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")
|
||||
log.Fatal().Err(err).Msg("Failed to add version command")
|
||||
}
|
||||
|
||||
err = cmdUser.AddCommand(verifyUserCmd())
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add user verify command")
|
||||
log.Fatal().Err(err).Msg("Failed to add verify command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(healthcheckCmd())
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add healthcheck command")
|
||||
log.Fatal().Err(err).Msg("Failed to add healthcheck command")
|
||||
}
|
||||
|
||||
err = cmdTotp.AddCommand(generateTotpCmd())
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add totp generate command")
|
||||
log.Fatal().Err(err).Msg("Failed to add generate command")
|
||||
}
|
||||
|
||||
err = cmdUser.AddCommand(createUserCmd())
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add create user command")
|
||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
||||
}
|
||||
|
||||
err = cmdOidc.AddCommand(createOidcClientCmd())
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add create oidc client command")
|
||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(cmdUser)
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add user command")
|
||||
log.Fatal().Err(err).Msg("Failed to add user command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(cmdTotp)
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add totp command")
|
||||
log.Fatal().Err(err).Msg("Failed to add totp command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(cmdOidc)
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add oidc command")
|
||||
log.Fatal().Err(err).Msg("Failed to add oidc command")
|
||||
}
|
||||
|
||||
err = cli.Execute(cmdTinyauth)
|
||||
|
||||
if err != nil {
|
||||
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")
|
||||
log.Fatal().Err(err).Msg("Failed to execute command")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,98 +124,3 @@ type themeBase struct{}
|
||||
func (t *themeBase) Theme(isDark bool) *huh.Styles {
|
||||
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
|
||||
}
|
||||
|
||||
+64
-70
@@ -3,9 +3,9 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/pquerna/otp/totp"
|
||||
@@ -38,87 +38,81 @@ func verifyUserCmd() *cli.Command {
|
||||
&cli.FlagLoader{},
|
||||
}
|
||||
|
||||
cmd := &cli.Command{
|
||||
return &cli.Command{
|
||||
Name: "verify",
|
||||
Description: "Verify a user is set up correctly",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
}
|
||||
Run: func(_ []string) error {
|
||||
log := logger.NewLogger().WithSimpleConfig()
|
||||
log.Init()
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
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 run interactive prompt: %w", err)
|
||||
return fmt.Errorf("failed to parse user: %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")
|
||||
if user.Username != tCfg.Username {
|
||||
return fmt.Errorf("username is incorrect")
|
||||
}
|
||||
fmt.Println(colors.green.Render("✓") + " User verified")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,10 +14,9 @@ func versionCmd() *cli.Command {
|
||||
Configuration: nil,
|
||||
Resources: nil,
|
||||
Run: func(_ []string) error {
|
||||
colors := getColors()
|
||||
fmt.Printf("Version: %s\n", colors.blue.Render(model.Version))
|
||||
fmt.Printf("Commit Hash: %s\n", colors.blue.Render(model.CommitHash))
|
||||
fmt.Printf("Build Timestamp: %s\n", colors.blue.Render(model.BuildTimestamp))
|
||||
fmt.Printf("Version: %s\n", model.Version)
|
||||
fmt.Printf("Commit Hash: %s\n", model.CommitHash)
|
||||
fmt.Printf("Build Timestamp: %s\n", model.BuildTimestamp)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.5.0",
|
||||
"prettier": "3.8.2",
|
||||
"rollup-plugin-visualizer": "^7.0.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~6.0.2",
|
||||
|
||||
Generated
+10
@@ -120,6 +120,9 @@ importers:
|
||||
globals:
|
||||
specifier: ^17.5.0
|
||||
version: 17.6.0
|
||||
prettier:
|
||||
specifier: 3.8.2
|
||||
version: 3.8.2
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(rolldown@1.0.1)
|
||||
@@ -2145,6 +2148,11 @@ packages:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
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:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
@@ -4650,6 +4658,8 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.8.2: {}
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
proxy-from-env@2.1.0: {}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
dangerouslyAllowAllBuilds: false
|
||||
blockExoticSubdeps: true
|
||||
minimumReleaseAge: 1440 # 1 day
|
||||
minimumReleaseAge: 1440 # 1 day
|
||||
trustPolicy: no-downgrade
|
||||
|
||||
@@ -62,11 +62,6 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/authorize/, ""),
|
||||
},
|
||||
"/swagger": {
|
||||
target: "http://tinyauth-backend:3000/swagger",
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/swagger/, ""),
|
||||
}
|
||||
},
|
||||
allowedHosts: true,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// gen/context_paths generates the ignore paths for the user context since
|
||||
// gin will not less apply the middleware to only specific paths.
|
||||
//
|
||||
// The generator reads every controller and looks for the //context:ignore comment.
|
||||
// The format for the context ignore comment is:
|
||||
//
|
||||
// //contxt:ignore /api/mypath GET,POST
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
//go:embed paths.tmpl
|
||||
var pathsTmplSrc string
|
||||
|
||||
var pathsTmpl = template.Must(template.New("paths").Parse(pathsTmplSrc))
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Printf("Failed to generate: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
// load pkg
|
||||
pkgConfig := &packages.Config{
|
||||
Mode: packages.NeedFiles,
|
||||
}
|
||||
|
||||
pkgs, err := packages.Load(pkgConfig, "github.com/tinyauthapp/tinyauth/internal/controller")
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load pkg: %w", err)
|
||||
}
|
||||
|
||||
if len(pkgs) == 0 {
|
||||
return fmt.Errorf("failed to get controllers package")
|
||||
}
|
||||
|
||||
pkg := pkgs[0]
|
||||
|
||||
// for each file we check the comments and either add or remove the context
|
||||
var contextIgnorePaths []string
|
||||
|
||||
for _, gofile := range pkg.GoFiles {
|
||||
// read the file
|
||||
file, err := os.ReadFile(gofile)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read %s, ignoring", gofile)
|
||||
continue
|
||||
}
|
||||
|
||||
// get the comment lines
|
||||
lines := strings.SplitSeq(string(file), "\n")
|
||||
|
||||
for line := range lines {
|
||||
if !strings.HasPrefix(strings.TrimSpace(line), "//context:ignore") {
|
||||
continue
|
||||
}
|
||||
|
||||
path, methods, ok := parseContextIgnoreLine(line)
|
||||
|
||||
if !ok {
|
||||
fmt.Printf("Failed to parse %s rule, ignore", line)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, m := range methods {
|
||||
contextIgnorePaths = append(contextIgnorePaths, m+" "+path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate out
|
||||
type tmplData struct {
|
||||
IgnorePaths []string
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := pathsTmpl.Execute(&buf, tmplData{
|
||||
IgnorePaths: contextIgnorePaths,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
formatted, err := format.Source(buf.Bytes())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("gofmt failed: %w", err)
|
||||
}
|
||||
|
||||
// write out
|
||||
err = os.WriteFile("context_paths.go", formatted, 0666)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write out: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseContextIgnoreLine(line string) (string, []string, bool) {
|
||||
line = strings.TrimPrefix(line, "//context:ignore ")
|
||||
path, methodStr, ok := strings.Cut(line, " ")
|
||||
if !ok {
|
||||
return "", []string{}, false
|
||||
}
|
||||
var methodsParsed []string
|
||||
methodParts := strings.SplitSeq(methodStr, ",")
|
||||
for m := range methodParts {
|
||||
if strings.TrimSpace(m) == "" {
|
||||
continue
|
||||
}
|
||||
m = strings.ToUpper(m)
|
||||
methodsParsed = append(methodsParsed, m)
|
||||
}
|
||||
return path, methodsParsed, true
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Code generated by gen/context_paths. DO NOT EDIT.
|
||||
package middleware
|
||||
|
||||
var contextSkipPathsPrefix = []string{
|
||||
{{range .IgnorePaths}}"{{.}}",
|
||||
{{end}}}
|
||||
@@ -1,3 +1,9 @@
|
||||
// gen/docs generates the .env.example and config.gen.md
|
||||
// files for the configuration of Tinyauth. Run via:
|
||||
//
|
||||
// The generator reads the Tinyauth configuration package and using reflection it generates the
|
||||
// example files. The .env.example is used in this repo while the config.gen.md is used in the
|
||||
// documentaton alongside some warnings that are added later.
|
||||
package main
|
||||
|
||||
import (
|
||||
+1
-1
@@ -20,7 +20,7 @@ type EnvEntry struct {
|
||||
}
|
||||
|
||||
func generateExampleEnv() {
|
||||
cfg := model.NewDefaultConfiguration(model.RuntimeEnvUnknown)
|
||||
cfg := model.NewDefaultConfiguration()
|
||||
entries := make([]EnvEntry, 0)
|
||||
|
||||
root := reflect.TypeOf(cfg).Elem()
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ type MarkdownEntry struct {
|
||||
}
|
||||
|
||||
func generateMarkdown() {
|
||||
cfg := model.NewDefaultConfiguration(model.RuntimeEnvUnknown)
|
||||
cfg := model.NewDefaultConfiguration()
|
||||
entries := make([]MarkdownEntry, 0)
|
||||
|
||||
root := reflect.TypeOf(cfg).Elem()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// gen/sqlc_wrapper generates store.go wrapper files for each sqlc driver package under
|
||||
// internal/repository/<driver>/. Run via:
|
||||
//
|
||||
// go generate ./internal/repository/...
|
||||
// internal/repository/<driver>/.
|
||||
//
|
||||
// The generator introspects *Queries methods and the model/params types in the
|
||||
// driver package, then emits a store.go that wraps *Queries so it satisfies
|
||||
@@ -32,7 +30,7 @@ import (
|
||||
var storeSrc string
|
||||
|
||||
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 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
|
||||
// Code generated by cmd/gen/sqlc_wrapper. DO NOT EDIT.
|
||||
package {{.PkgName}}
|
||||
|
||||
import (
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
package tinyauth
|
||||
package docs
|
||||
|
||||
//go:generate go run github.com/tinyauthapp/tinyauth/gen/docs
|
||||
|
||||
@@ -4,7 +4,6 @@ go 1.26.4
|
||||
|
||||
require (
|
||||
charm.land/huh/v2 v2.0.3
|
||||
charm.land/lipgloss/v2 v2.0.1
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
@@ -20,14 +19,12 @@ require (
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/steveiliop56/ding v0.2.0
|
||||
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/weppos/publicsuffix-go v0.50.3
|
||||
go.uber.org/dig v1.19.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/tools v0.47.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.36.2
|
||||
k8s.io/client-go v0.36.2
|
||||
modernc.org/sqlite v1.53.0
|
||||
@@ -37,11 +34,11 @@ require (
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.0.0 // indirect
|
||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||
charm.land/lipgloss/v2 v2.0.1 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // 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/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
@@ -85,10 +82,6 @@ require (
|
||||
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/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/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
@@ -105,14 +98,12 @@ require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.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-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
@@ -143,8 +134,6 @@ require (
|
||||
github.com/safchain/ethtool v0.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.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/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
|
||||
@@ -180,6 +169,7 @@ require (
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
|
||||
@@ -20,8 +20,6 @@ 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/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
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/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
@@ -32,8 +30,6 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe
|
||||
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/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/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
@@ -137,7 +133,6 @@ 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/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||
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/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -197,17 +192,10 @@ 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-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-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/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/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/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -309,11 +297,8 @@ 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/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
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/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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
@@ -322,9 +307,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
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/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/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
@@ -379,7 +361,6 @@ 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/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/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/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -433,7 +414,6 @@ 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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@@ -441,12 +421,6 @@ 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
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/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
|
||||
@@ -489,7 +463,6 @@ 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/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/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/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
@@ -526,8 +499,6 @@ 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=
|
||||
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/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/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
@@ -536,54 +507,27 @@ 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/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
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/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/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-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-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/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.5.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/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/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/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-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/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/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
@@ -598,19 +542,13 @@ 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/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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/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/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
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-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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
|
||||
@@ -279,7 +279,7 @@ func (app *BootstrapApp) Setup() error {
|
||||
app.runtime.ConfiguredProviders = configuredProviders
|
||||
|
||||
// if tailscale is enabled and listening, replace the app url with the tailscale hostname
|
||||
if app.services.tailscaleService != nil && app.config.Experimental.Tailscale.Listen {
|
||||
if app.services.tailscaleService != nil && app.config.Tailscale.Listen {
|
||||
tailscaleUrl := "https://" + app.services.tailscaleService.GetHostname()
|
||||
|
||||
// if the tailscale url is different from the app url, replace it
|
||||
|
||||
@@ -6,28 +6,17 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
swaggerfiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
docs "github.com/tinyauthapp/tinyauth/internal/swagger"
|
||||
"go.uber.org/dig"
|
||||
|
||||
"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 {
|
||||
// we don't want gin debug mode
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
@@ -91,12 +80,6 @@ func (app *BootstrapApp) setupRouter() error {
|
||||
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{
|
||||
controller.NewContextController,
|
||||
controller.NewOAuthController,
|
||||
@@ -142,50 +125,14 @@ func (app *BootstrapApp) setupRouter() error {
|
||||
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
|
||||
// 1. Tailscale (if tailscale.listen)
|
||||
// 2. Unix socket (if server.socketPath)
|
||||
// 3. HTTP - default
|
||||
func (app *BootstrapApp) getListenerFunc() (func(ctx context.Context) error, error) {
|
||||
if app.config.Experimental.Tailscale.Listen {
|
||||
if app.config.Tailscale.Listen {
|
||||
if app.services.tailscaleService == nil {
|
||||
return nil, fmt.Errorf("experimental.tailscale.listen is enabled but tailscale service is not initialized")
|
||||
return nil, fmt.Errorf("tailscale.listen is enabled but tailscale service is not initialized")
|
||||
}
|
||||
return app.serveTailscale, nil
|
||||
}
|
||||
@@ -280,6 +227,7 @@ func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, ctx c
|
||||
err := server.Serve(listener)
|
||||
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
shutdown()
|
||||
return fmt.Errorf("failed to start %s listener: %w", name, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -107,14 +107,6 @@ func NewContextController(i ContextControllerInput) *ContextController {
|
||||
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) {
|
||||
context, err := new(model.UserContext).NewFromGin(c)
|
||||
|
||||
@@ -155,14 +147,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||
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]
|
||||
//context:ignore /api/context/app GET
|
||||
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
||||
c.JSON(200, AppContextResponse{
|
||||
Status: 200,
|
||||
|
||||
@@ -7,10 +7,6 @@ const (
|
||||
FrontendLoginForApp FrontendLoginFor = "app"
|
||||
)
|
||||
|
||||
type SimpleResponse struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
type UnauthorizedQuery struct {
|
||||
Username string `url:"username"`
|
||||
Resource string `url:"resource"`
|
||||
|
||||
@@ -23,18 +23,10 @@ func NewHealthController(i HealthControllerInput) *HealthController {
|
||||
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]
|
||||
//context:ignore /api/healthz GET,HEAD
|
||||
func (controller *HealthController) healthHandler(c *gin.Context) {
|
||||
c.JSON(200, SimpleResponse{
|
||||
Status: 200,
|
||||
Message: "OK",
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Healthy",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,27 +54,7 @@ func NewOAuthController(i OAuthControllerInput) *OAuthController {
|
||||
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]
|
||||
//context:ignore /api/oauth/url GET
|
||||
func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||
var req OAuthRequest
|
||||
|
||||
@@ -132,33 +112,24 @@ 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.JSON(200, OAuthURLSuccessResponse{
|
||||
SimpleResponse: SimpleResponse{
|
||||
Status: 200,
|
||||
Message: "OK",
|
||||
},
|
||||
URL: authUrl,
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"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]
|
||||
//context:ignore /api/oauth/callback GET
|
||||
func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
var req OAuthRequest
|
||||
|
||||
err := c.BindUri(&req)
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get provider ID")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
controller.log.App.Error().Err(err).Msg("Failed to bind URI")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -166,7 +137,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get OAuth session cookie")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -176,7 +147,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get pending OAuth session")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -185,7 +156,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
state := c.Query("state")
|
||||
if state != oauthPendingSession.State {
|
||||
controller.log.App.Warn().Msg("OAuth state mismatch")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,7 +165,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to exchange code for token")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -202,19 +173,19 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get user info from OAuth provider")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
controller.log.App.Warn().Msg("OAuth provider did not return user info")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if user.Email == "" {
|
||||
controller.log.App.Warn().Msg("OAuth provider did not return an email")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -222,13 +193,13 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get OAuth service for session")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if svc.ID() != req.Provider {
|
||||
controller.log.App.Warn().Msgf("OAuth provider mismatch: expected %s, got %s", req.Provider, svc.ID())
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -242,11 +213,11 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/unauthorized?%s", controller.runtime.AppURL, queries.Encode()))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.runtime.AppURL, queries.Encode()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -291,7 +262,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -304,10 +275,10 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
queries, err := query.Values(oauthPendingSession.CallbackParams)
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to encode OIDC callback query")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode()))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -319,15 +290,15 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to encode redirect query")
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("%s/continue?%s", controller.runtime.AppURL, queries.Encode()))
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.runtime.AppURL, queries.Encode()))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, controller.runtime.AppURL)
|
||||
c.Redirect(http.StatusTemporaryRedirect, controller.runtime.AppURL)
|
||||
}
|
||||
|
||||
func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackParams) bool {
|
||||
|
||||
@@ -82,15 +82,6 @@ type AuthorizeCompleteRequest struct {
|
||||
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 {
|
||||
dig.In
|
||||
|
||||
@@ -123,36 +114,6 @@ func NewOIDCController(i OIDCControllerInput) *OIDCController {
|
||||
// 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
|
||||
// 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) {
|
||||
if controller.oidc == nil {
|
||||
controller.authorizeError(c, authorizeErrorParams{
|
||||
@@ -300,16 +261,6 @@ func (controller *OIDCController) authorize(c *gin.Context) {
|
||||
|
||||
// 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.
|
||||
// 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) {
|
||||
if controller.oidc == nil {
|
||||
// For this endpoint we return JSON errors since it's called
|
||||
@@ -410,44 +361,18 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, AuthorizeCompleteResponse{
|
||||
SimpleResponse: SimpleResponse{
|
||||
Status: 200,
|
||||
},
|
||||
RedirectURI: fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()),
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"redirect_uri": 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]
|
||||
//context:ignore /api/oidc/token POST
|
||||
func (controller *OIDCController) Token(c *gin.Context) {
|
||||
if controller.oidc == nil {
|
||||
controller.log.App.Warn().Msg("Received OIDC request but OIDC server is not configured")
|
||||
c.JSON(500, OIDCErrorResponse{
|
||||
Error: "server_error",
|
||||
c.JSON(500, gin.H{
|
||||
"error": "server_error",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -457,8 +382,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
err := c.Bind(&req)
|
||||
if err != nil {
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to bind token request")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_request",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -466,8 +391,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
err = controller.oidc.ValidateGrantType(req.GrantType)
|
||||
if err != nil {
|
||||
controller.log.App.Warn().Err(err).Msg("Invalid grant type")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: err.Error(),
|
||||
c.JSON(400, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -487,8 +412,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
if !ok {
|
||||
controller.log.App.Warn().Msg("Client credentials not found in basic auth")
|
||||
c.Header("www-authenticate", `Basic realm="Tinyauth OIDC Token Endpoint"`)
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_client",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_client",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -503,16 +428,16 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
|
||||
if !ok {
|
||||
controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Client not found")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_client",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_client",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if client.ClientSecret != creds.ClientSecret {
|
||||
controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Invalid client secret")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_client",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_client",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -533,15 +458,15 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to delete session for reused code")
|
||||
}
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_grant",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
controller.log.App.Warn().Msg("Code not found")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_grant",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -551,8 +476,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
|
||||
if entry.RedirectURI != req.RedirectURI {
|
||||
controller.log.App.Warn().Msg("Redirect URI does not match")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_grant",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -561,8 +486,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
|
||||
if !ok {
|
||||
controller.log.App.Warn().Msg("PKCE validation failed")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_grant",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -571,8 +496,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to generate access token")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "server_error",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "server_error",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -584,23 +509,23 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTokenExpired) {
|
||||
controller.log.App.Warn().Msg("Refresh token expired")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_grant",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, service.ErrInvalidClient) {
|
||||
controller.log.App.Warn().Msg("Refresh token does not belong to client")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_grant",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
controller.log.App.Error().Err(err).Msg("Failed to refresh access token")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "server_error",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "server_error",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -614,25 +539,12 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
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]
|
||||
//context:ignore /api/oidc/userinfo GET,POST
|
||||
func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
if controller.oidc == nil {
|
||||
controller.log.App.Warn().Msg("Received OIDC userinfo request but OIDC server is not configured")
|
||||
c.JSON(500, OIDCErrorResponse{
|
||||
Error: "server_error",
|
||||
c.JSON(500, gin.H{
|
||||
"error": "server_error",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -644,16 +556,16 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
tokenType, bearerToken, ok := strings.Cut(authorization, " ")
|
||||
if !ok {
|
||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid authorization header")
|
||||
c.JSON(401, OIDCErrorResponse{
|
||||
Error: "invalid_request",
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.ToLower(tokenType) != "bearer" {
|
||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with non-bearer token")
|
||||
c.JSON(401, OIDCErrorResponse{
|
||||
Error: "invalid_request",
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -662,23 +574,23 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
} else if c.Request.Method == http.MethodPost {
|
||||
if c.ContentType() != "application/x-www-form-urlencoded" {
|
||||
controller.log.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type")
|
||||
c.JSON(400, OIDCErrorResponse{
|
||||
Error: "invalid_request",
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
token = c.PostForm("access_token")
|
||||
if token == "" {
|
||||
controller.log.App.Warn().Msg("OIDC userinfo POST accessed without access_token")
|
||||
c.JSON(401, OIDCErrorResponse{
|
||||
Error: "invalid_request",
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
controller.log.App.Warn().Msg("OIDC userinfo accessed without authorization header or POST body")
|
||||
c.JSON(401, OIDCErrorResponse{
|
||||
Error: "invalid_request",
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -688,15 +600,15 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTokenNotFound) {
|
||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
||||
c.JSON(401, OIDCErrorResponse{
|
||||
Error: "invalid_grant",
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get access token")
|
||||
c.JSON(401, OIDCErrorResponse{
|
||||
Error: "server_error",
|
||||
c.JSON(401, gin.H{
|
||||
"error": "server_error",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -704,8 +616,8 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
// If we don't have the openid scope, return an error
|
||||
if !slices.Contains(strings.Split(entry.Scope, " "), "openid") {
|
||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with missing openid scope")
|
||||
c.JSON(401, OIDCErrorResponse{
|
||||
Error: "invalid_scope",
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_scope",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -716,8 +628,8 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to get user info")
|
||||
c.JSON(401, OIDCErrorResponse{
|
||||
Error: "server_error",
|
||||
c.JSON(401, gin.H{
|
||||
"error": "server_error",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -752,11 +664,9 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz
|
||||
redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode())
|
||||
|
||||
if params.json {
|
||||
c.JSON(200, AuthorizeCompleteResponse{
|
||||
SimpleResponse: SimpleResponse{
|
||||
Status: 200,
|
||||
},
|
||||
RedirectURI: redirectUrl,
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"redirect_uri": redirectUrl,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -786,11 +696,9 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz
|
||||
}
|
||||
|
||||
if params.json {
|
||||
c.JSON(200, AuthorizeCompleteResponse{
|
||||
SimpleResponse: SimpleResponse{
|
||||
Status: 200,
|
||||
},
|
||||
RedirectURI: redirectUrl,
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"redirect_uri": redirectUrl,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ func NewResourcesController(i ResourcesControllerInput) *ResourcesController {
|
||||
return controller
|
||||
}
|
||||
|
||||
//context:ignore /resources GET
|
||||
func (controller *ResourcesController) resourcesHandler(c *gin.Context) {
|
||||
if controller.config.Resources.Path == "" {
|
||||
c.JSON(404, gin.H{
|
||||
|
||||
@@ -57,6 +57,7 @@ func NewUserController(i UserControllerInput) *UserController {
|
||||
return controller
|
||||
}
|
||||
|
||||
//context:ignore /api/user/login POST
|
||||
func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ func NewWellKnownController(i WellKnownControllerInput) *WellKnownController {
|
||||
return controller
|
||||
}
|
||||
|
||||
//context:ignore /.well-known/openid-configuration GET
|
||||
func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context) {
|
||||
if controller.oidc == nil {
|
||||
c.JSON(500, gin.H{
|
||||
@@ -94,6 +95,7 @@ func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context
|
||||
})
|
||||
}
|
||||
|
||||
//context:ignore /.well-known/jwks.json GET
|
||||
func (controller *WellKnownController) JWKS(c *gin.Context) {
|
||||
if controller.oidc == nil {
|
||||
c.JSON(500, gin.H{
|
||||
@@ -122,6 +124,7 @@ func (controller *WellKnownController) JWKS(c *gin.Context) {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
//context:ignore /.well-known/webfinger GET
|
||||
func (controller *WellKnownController) WebFinger(c *gin.Context) {
|
||||
c.Header("Content-Type", "application/jrd+json")
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
|
||||
@@ -16,26 +16,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Gin won't let us set a middleware on a specific route (at least it doesn't work,
|
||||
// see https://github.com/gin-gonic/gin/issues/531) so we have to do some hackery
|
||||
var (
|
||||
contextSkipPathsPrefix = []string{
|
||||
"GET /api/context/app",
|
||||
"GET /api/healthz",
|
||||
"HEAD /api/healthz",
|
||||
"GET /api/oauth/url",
|
||||
"GET /api/oauth/callback",
|
||||
"GET /api/oidc/clients",
|
||||
"POST /api/oidc/token",
|
||||
"GET /api/oidc/userinfo",
|
||||
"POST /api/oidc/userinfo",
|
||||
"GET /resources",
|
||||
"POST /api/user/login",
|
||||
"GET /.well-known/openid-configuration",
|
||||
"GET /.well-known/jwks.json",
|
||||
}
|
||||
)
|
||||
|
||||
type ContextMiddleware struct {
|
||||
log *logger.Logger
|
||||
runtime *model.RuntimeConfig
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Code generated by gen/context_paths. DO NOT EDIT.
|
||||
package middleware
|
||||
|
||||
var contextSkipPathsPrefix = []string{
|
||||
"GET /api/context/app",
|
||||
"GET /api/healthz",
|
||||
"HEAD /api/healthz",
|
||||
"GET /api/oauth/url",
|
||||
"GET /api/oauth/callback",
|
||||
"POST /api/oidc/token",
|
||||
"GET /api/oidc/userinfo",
|
||||
"POST /api/oidc/userinfo",
|
||||
"GET /resources",
|
||||
"POST /api/user/login",
|
||||
"GET /.well-known/openid-configuration",
|
||||
"GET /.well-known/jwks.json",
|
||||
"GET /.well-known/webfinger",
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package middleware
|
||||
|
||||
//go:generate go run github.com/tinyauthapp/tinyauth/gen/context_paths
|
||||
@@ -44,7 +44,7 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc {
|
||||
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||
|
||||
switch strings.SplitN(path, "/", 2)[0] {
|
||||
case "api", "resources", ".well-known", "authorize", "swagger":
|
||||
case "api", "resources", ".well-known", "authorize":
|
||||
c.Next()
|
||||
return
|
||||
case "robots.txt":
|
||||
|
||||
+144
-176
@@ -1,27 +1,8 @@
|
||||
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
|
||||
func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
|
||||
cfg := &Config{
|
||||
func NewDefaultConfiguration() *Config {
|
||||
return &Config{
|
||||
Database: DatabaseConfig{
|
||||
Driver: "sqlite",
|
||||
Path: "./tinyauth.db",
|
||||
@@ -81,259 +62,246 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
|
||||
PrivateKeyPath: "./tinyauth_oidc_key",
|
||||
PublicKeyPath: "./tinyauth_oidc_key.pub",
|
||||
},
|
||||
Experimental: ExperimentalConfig{
|
||||
Tailscale: TailscaleConfig{
|
||||
Dir: "./tailscale_state",
|
||||
},
|
||||
Tailscale: TailscaleConfig{
|
||||
Dir: "./tailscale_state",
|
||||
},
|
||||
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 {
|
||||
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"`
|
||||
Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"`
|
||||
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"`
|
||||
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"`
|
||||
Server ServerConfig `description:"Server configuration." yaml:"server,omitempty"`
|
||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth,omitempty"`
|
||||
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps,omitempty"`
|
||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth,omitempty"`
|
||||
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc,omitempty"`
|
||||
UI UIConfig `description:"UI customization." yaml:"ui,omitempty"`
|
||||
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"`
|
||||
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,omitempty"`
|
||||
Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"`
|
||||
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
||||
Database DatabaseConfig `description:"Database configuration." yaml:"database"`
|
||||
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics"`
|
||||
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources"`
|
||||
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
||||
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
||||
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider"`
|
||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"`
|
||||
ConfigFile string `description:"Path to config file." yaml:"-"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
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,omitempty"`
|
||||
Driver string `description:"The database driver to use. Valid values: sqlite, postgres, memory." yaml:"driver"`
|
||||
Path string `description:"The path to the SQLite database file, or connection URL when driver is postgres." yaml:"path"`
|
||||
}
|
||||
|
||||
type AnalyticsConfig struct {
|
||||
Enabled bool `description:"Enable periodic version information collection." yaml:"enabled,omitempty"`
|
||||
Enabled bool `description:"Enable periodic version information collection." yaml:"enabled"`
|
||||
}
|
||||
|
||||
type ResourcesConfig struct {
|
||||
Enabled bool `description:"Enable the resources server." yaml:"enabled,omitempty"`
|
||||
Path string `description:"The directory where resources are stored." yaml:"path,omitempty"`
|
||||
Enabled bool `description:"Enable the resources server." yaml:"enabled"`
|
||||
Path string `description:"The directory where resources are stored." yaml:"path"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
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,omitempty"`
|
||||
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath,omitempty"`
|
||||
Port int `description:"The port on which the server listens." yaml:"port"`
|
||||
Address string `description:"The address on which the server listens." yaml:"address"`
|
||||
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
IP IPConfig `description:"IP whitelisting config options." yaml:"ip,omitempty"`
|
||||
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users,omitempty"`
|
||||
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled,omitempty"`
|
||||
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,omitempty"`
|
||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie,omitempty"`
|
||||
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry,omitempty"`
|
||||
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime,omitempty"`
|
||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout,omitempty"`
|
||||
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,omitempty"`
|
||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies,omitempty"`
|
||||
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls,omitempty"`
|
||||
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
||||
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
||||
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled"`
|
||||
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"`
|
||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
||||
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
||||
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"`
|
||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||
LockdownEnabled bool `description:"Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically." yaml:"lockdownEnabled"`
|
||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls"`
|
||||
}
|
||||
|
||||
type UserAttributes struct {
|
||||
Name string `description:"Full name of the user." yaml:"name,omitempty"`
|
||||
GivenName string `description:"Given (first) name of the user." yaml:"givenName,omitempty"`
|
||||
FamilyName string `description:"Family (last) name of the user." yaml:"familyName,omitempty"`
|
||||
MiddleName string `description:"Middle name of the user." yaml:"middleName,omitempty"`
|
||||
Nickname string `description:"Nickname of the user." yaml:"nickname,omitempty"`
|
||||
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,omitempty"`
|
||||
Website string `description:"URL of the user's website." yaml:"website,omitempty"`
|
||||
Email string `description:"Email address of the user." yaml:"email,omitempty"`
|
||||
Gender string `description:"Gender of the user." yaml:"gender,omitempty"`
|
||||
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,omitempty"`
|
||||
Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale,omitempty"`
|
||||
PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber,omitempty"`
|
||||
Address AddressClaim `description:"Address of the user." yaml:"address,omitempty"`
|
||||
Name string `description:"Full name of the user." yaml:"name"`
|
||||
GivenName string `description:"Given (first) name of the user." yaml:"givenName"`
|
||||
FamilyName string `description:"Family (last) name of the user." yaml:"familyName"`
|
||||
MiddleName string `description:"Middle name of the user." yaml:"middleName"`
|
||||
Nickname string `description:"Nickname of the user." yaml:"nickname"`
|
||||
Profile string `description:"URL of the user's profile page." yaml:"profile"`
|
||||
Picture string `description:"URL of the user's profile picture." yaml:"picture"`
|
||||
Website string `description:"URL of the user's website." yaml:"website"`
|
||||
Email string `description:"Email address of the user." yaml:"email"`
|
||||
Gender string `description:"Gender of the user." yaml:"gender"`
|
||||
Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate"`
|
||||
Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo"`
|
||||
Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale"`
|
||||
PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber"`
|
||||
Address AddressClaim `description:"Address of the user." yaml:"address"`
|
||||
}
|
||||
|
||||
type AddressClaim struct {
|
||||
Formatted string `description:"Full mailing address, formatted for display." yaml:"formatted,omitempty" json:"formatted,omitempty"`
|
||||
StreetAddress string `description:"Street address." yaml:"streetAddress,omitempty" json:"street_address,omitempty"`
|
||||
Locality string `description:"City or locality." yaml:"locality,omitempty" json:"locality,omitempty"`
|
||||
Region string `description:"State, province, or region." yaml:"region,omitempty" json:"region,omitempty"`
|
||||
PostalCode string `description:"Zip or postal code." yaml:"postalCode,omitempty" json:"postal_code,omitempty"`
|
||||
Country string `description:"Country." yaml:"country,omitempty" json:"country,omitempty"`
|
||||
Formatted string `description:"Full mailing address, formatted for display." yaml:"formatted" json:"formatted,omitempty"`
|
||||
StreetAddress string `description:"Street address." yaml:"streetAddress" json:"street_address,omitempty"`
|
||||
Locality string `description:"City or locality." yaml:"locality" json:"locality,omitempty"`
|
||||
Region string `description:"State, province, or region." yaml:"region" json:"region,omitempty"`
|
||||
PostalCode string `description:"Zip or postal code." yaml:"postalCode" json:"postal_code,omitempty"`
|
||||
Country string `description:"Country." yaml:"country" json:"country,omitempty"`
|
||||
}
|
||||
|
||||
type IPConfig struct {
|
||||
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,omitempty"`
|
||||
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication entirely." yaml:"bypass,omitempty"`
|
||||
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
||||
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
||||
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication entirely." yaml:"bypass"`
|
||||
}
|
||||
|
||||
type OAuthConfig struct {
|
||||
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist,omitempty"`
|
||||
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,omitempty"`
|
||||
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers,omitempty"`
|
||||
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
||||
WhitelistFile string `description:"Path to the OAuth whitelist file." yaml:"whitelistFile"`
|
||||
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
|
||||
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
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,omitempty"`
|
||||
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients,omitempty"`
|
||||
PrivateKeyPath string `description:"Path to the private key file, including file name." yaml:"privateKeyPath"`
|
||||
PublicKeyPath string `description:"Path to the public key file, including file name." yaml:"publicKeyPath"`
|
||||
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients"`
|
||||
}
|
||||
|
||||
type UIConfig struct {
|
||||
Title string `description:"The title of the UI." yaml:"title,omitempty"`
|
||||
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage,omitempty"`
|
||||
BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage,omitempty"`
|
||||
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled,omitempty"`
|
||||
Title string `description:"The title of the UI." yaml:"title"`
|
||||
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"`
|
||||
BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage"`
|
||||
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled"`
|
||||
}
|
||||
|
||||
type LDAPConfig struct {
|
||||
Address string `description:"LDAP server address." yaml:"address,omitempty"`
|
||||
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn,omitempty"`
|
||||
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword,omitempty"`
|
||||
BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile,omitempty"`
|
||||
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn,omitempty"`
|
||||
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure,omitempty"`
|
||||
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter,omitempty"`
|
||||
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert,omitempty"`
|
||||
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey,omitempty"`
|
||||
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL,omitempty"`
|
||||
Address string `description:"LDAP server address." yaml:"address"`
|
||||
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
||||
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
||||
BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile"`
|
||||
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
||||
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
||||
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
||||
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
|
||||
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
||||
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level,omitempty"`
|
||||
Json bool `description:"Enable JSON formatted logs." yaml:"json,omitempty"`
|
||||
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams,omitempty"`
|
||||
Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level"`
|
||||
Json bool `description:"Enable JSON formatted logs." yaml:"json"`
|
||||
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams"`
|
||||
}
|
||||
|
||||
type LogStreams struct {
|
||||
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http,omitempty"`
|
||||
App LogStreamConfig `description:"Application logging." yaml:"app,omitempty"`
|
||||
Audit LogStreamConfig `description:"Audit logging." yaml:"audit,omitempty"`
|
||||
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http"`
|
||||
App LogStreamConfig `description:"Application logging." yaml:"app"`
|
||||
Audit LogStreamConfig `description:"Audit logging." yaml:"audit"`
|
||||
}
|
||||
|
||||
type LogStreamConfig struct {
|
||||
Enabled bool `description:"Enable this log stream." yaml:"enabled,omitempty"`
|
||||
Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"`
|
||||
Enabled bool `description:"Enable this log stream." yaml:"enabled"`
|
||||
Level string `description:"Log level for this stream. Use global if empty." yaml:"level"`
|
||||
}
|
||||
|
||||
type ExperimentalConfig struct {
|
||||
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"`
|
||||
}
|
||||
// no experimental features
|
||||
type ExperimentalConfig struct{}
|
||||
|
||||
type TailscaleConfig struct {
|
||||
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"`
|
||||
Dir string `description:"Tailscale state directory." yaml:"dir,omitempty"`
|
||||
Hostname string `description:"Tailscale hostname." yaml:"hostname,omitempty"`
|
||||
AuthKey string `description:"Tailscale auth key." yaml:"authKey,omitempty"`
|
||||
Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral,omitempty"`
|
||||
Funnel bool `description:"Enable Tailscale Funnel." yaml:"funnel,omitempty"`
|
||||
Listen bool `description:"Listen on the Tailscale address instead of standard address." yaml:"listen,omitempty"`
|
||||
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled"`
|
||||
Dir string `description:"Tailscale state directory." yaml:"dir"`
|
||||
Hostname string `description:"Tailscale hostname." yaml:"hostname"`
|
||||
AuthKey string `description:"Tailscale auth key." yaml:"authKey"`
|
||||
Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral"`
|
||||
Funnel bool `description:"Enable Tailscale Funnel." yaml:"funnel"`
|
||||
Listen bool `description:"Listen on the Tailscale address instead of standard address." yaml:"listen"`
|
||||
}
|
||||
|
||||
// OAuth/OIDC config
|
||||
|
||||
type OAuthServiceConfig struct {
|
||||
ClientID string `description:"OAuth client ID." yaml:"clientId,omitempty"`
|
||||
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret,omitempty"`
|
||||
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,omitempty"`
|
||||
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile,omitempty"`
|
||||
Scopes []string `description:"OAuth scopes." yaml:"scopes,omitempty"`
|
||||
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl,omitempty"`
|
||||
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl,omitempty"`
|
||||
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl,omitempty"`
|
||||
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl,omitempty"`
|
||||
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure,omitempty"`
|
||||
Name string `description:"Provider name in UI." yaml:"name,omitempty"`
|
||||
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
||||
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
||||
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile"`
|
||||
Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist"`
|
||||
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile"`
|
||||
Scopes []string `description:"OAuth scopes." yaml:"scopes"`
|
||||
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl"`
|
||||
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl"`
|
||||
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl"`
|
||||
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl"`
|
||||
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure"`
|
||||
Name string `description:"Provider name in UI." yaml:"name"`
|
||||
}
|
||||
|
||||
type OIDCClientConfig struct {
|
||||
ID string `description:"OIDC client ID." yaml:"-"`
|
||||
ClientID string `description:"OIDC client ID." yaml:"clientId,omitempty"`
|
||||
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret,omitempty"`
|
||||
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,omitempty"`
|
||||
Name string `description:"Client name in UI." yaml:"name,omitempty"`
|
||||
ClientID string `description:"OIDC client ID." yaml:"clientId"`
|
||||
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret"`
|
||||
ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile"`
|
||||
TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris"`
|
||||
Name string `description:"Client name in UI." yaml:"name"`
|
||||
}
|
||||
|
||||
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,omitempty"`
|
||||
Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy"`
|
||||
}
|
||||
|
||||
// ACLs
|
||||
|
||||
type Apps struct {
|
||||
Apps map[string]App `description:"App ACLs configuration." yaml:"apps,omitempty"`
|
||||
Apps map[string]App `description:"App ACLs configuration." yaml:"apps"`
|
||||
}
|
||||
|
||||
type App struct {
|
||||
Config AppConfig `description:"App configuration." yaml:"config,omitempty"`
|
||||
Users AppUsers `description:"User access configuration." yaml:"users,omitempty"`
|
||||
OAuth AppOAuth `description:"OAuth access configuration." yaml:"oauth,omitempty"`
|
||||
IP AppIP `description:"IP access configuration." yaml:"ip,omitempty"`
|
||||
Response AppResponse `description:"Response customization." yaml:"response,omitempty"`
|
||||
Path AppPath `description:"Path access configuration." yaml:"path,omitempty"`
|
||||
LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap,omitempty"`
|
||||
Config AppConfig `description:"App configuration." yaml:"config"`
|
||||
Users AppUsers `description:"User access configuration." yaml:"users"`
|
||||
OAuth AppOAuth `description:"OAuth access configuration." yaml:"oauth"`
|
||||
IP AppIP `description:"IP access configuration." yaml:"ip"`
|
||||
Response AppResponse `description:"Response customization." yaml:"response"`
|
||||
Path AppPath `description:"Path access configuration." yaml:"path"`
|
||||
LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Domain string `description:"The domain of the app." yaml:"domain,omitempty"`
|
||||
Domain string `description:"The domain of the app." yaml:"domain"`
|
||||
}
|
||||
|
||||
type AppUsers struct {
|
||||
Allow string `description:"Comma-separated list of allowed users." yaml:"allow,omitempty"`
|
||||
Block string `description:"Comma-separated list of blocked users." yaml:"block,omitempty"`
|
||||
Allow string `description:"Comma-separated list of allowed users." yaml:"allow"`
|
||||
Block string `description:"Comma-separated list of blocked users." yaml:"block"`
|
||||
}
|
||||
|
||||
type AppOAuth struct {
|
||||
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,omitempty"`
|
||||
Whitelist string `description:"Comma-separated list of allowed OAuth groups." yaml:"whitelist"`
|
||||
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups"`
|
||||
}
|
||||
|
||||
type AppLDAP struct {
|
||||
Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups,omitempty"`
|
||||
Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups"`
|
||||
}
|
||||
|
||||
type AppIP struct {
|
||||
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,omitempty"`
|
||||
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication." yaml:"bypass,omitempty"`
|
||||
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
||||
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
||||
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication." yaml:"bypass"`
|
||||
}
|
||||
|
||||
type AppResponse struct {
|
||||
Headers []string `description:"Custom headers to add to the response." yaml:"headers,omitempty"`
|
||||
BasicAuth AppBasicAuth `description:"Basic authentication for the app." yaml:"basicAuth,omitempty"`
|
||||
Headers []string `description:"Custom headers to add to the response." yaml:"headers"`
|
||||
BasicAuth AppBasicAuth `description:"Basic authentication for the app." yaml:"basicAuth"`
|
||||
}
|
||||
|
||||
type AppBasicAuth struct {
|
||||
Username string `description:"Basic auth username." yaml:"username,omitempty"`
|
||||
Password string `description:"Basic auth password." yaml:"password,omitempty"`
|
||||
PasswordFile string `description:"Path to the file containing the basic auth password." yaml:"passwordFile,omitempty"`
|
||||
Username string `description:"Basic auth username." yaml:"username"`
|
||||
Password string `description:"Basic auth password." yaml:"password"`
|
||||
PasswordFile string `description:"Path to the file containing the basic auth password." yaml:"passwordFile"`
|
||||
}
|
||||
|
||||
type AppPath struct {
|
||||
Allow string `description:"Comma-separated list of allowed paths." yaml:"allow,omitempty"`
|
||||
Block string `description:"Comma-separated list of blocked paths." yaml:"block,omitempty"`
|
||||
Allow string `description:"Comma-separated list of allowed paths." yaml:"allow"`
|
||||
Block string `description:"Comma-separated list of blocked paths." yaml:"block"`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
|
||||
// Code generated by cmd/gen/sqlc_wrapper. DO NOT EDIT.
|
||||
package postgres
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
|
||||
// Code generated by cmd/gen/sqlc_wrapper. DO NOT EDIT.
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
|
||||
@@ -45,17 +45,17 @@ type TailscaleServiceInput struct {
|
||||
}
|
||||
|
||||
func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
|
||||
if !i.Config.Experimental.Tailscale.Enabled {
|
||||
if !i.Config.Tailscale.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
srv := new(tsnet.Server)
|
||||
|
||||
// node options
|
||||
srv.Dir = i.Config.Experimental.Tailscale.Dir
|
||||
srv.Hostname = i.Config.Experimental.Tailscale.Hostname
|
||||
srv.AuthKey = i.Config.Experimental.Tailscale.AuthKey
|
||||
srv.Ephemeral = i.Config.Experimental.Tailscale.Ephemeral
|
||||
srv.Dir = i.Config.Tailscale.Dir
|
||||
srv.Hostname = i.Config.Tailscale.Hostname
|
||||
srv.AuthKey = i.Config.Tailscale.AuthKey
|
||||
srv.Ephemeral = i.Config.Tailscale.Ephemeral
|
||||
|
||||
// redirect logs to zerolog
|
||||
srv.Logf = i.Log.App.Printf
|
||||
@@ -94,7 +94,7 @@ func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
|
||||
|
||||
i.Ding.Go(service.watchAndClose, ding.RingMajor)
|
||||
|
||||
if i.Config.Experimental.Tailscale.Funnel && !i.Config.Experimental.Tailscale.Listen {
|
||||
if i.Config.Tailscale.Funnel && !i.Config.Tailscale.Listen {
|
||||
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
|
||||
}
|
||||
|
||||
if ts.config.Experimental.Tailscale.Funnel {
|
||||
if ts.config.Tailscale.Funnel {
|
||||
ln, err := ts.srv.ListenFunnel("tcp", ":443")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,690 +0,0 @@
|
||||
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