mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-12-18 06:02:29 +00:00
Compare commits
3 Commits
feat/unifi
...
l10n_main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f8cf471b0 | ||
|
|
6b2cf3dec2 | ||
|
|
745c3eca36 |
106
.env.example
106
.env.example
@@ -1,84 +1,22 @@
|
||||
# Base Configuration
|
||||
|
||||
# The base URL where Tinyauth is accessible
|
||||
TINYAUTH_APPURL=https://auth.example.com
|
||||
# Log level: trace, debug, info, warn, error
|
||||
TINYAUTH_LOGLEVEL=info
|
||||
# Directory for static resources
|
||||
TINYAUTH_RESOURCESDIR=/data/resources
|
||||
# Path to SQLite database file
|
||||
TINYAUTH_DATABASEPATH=/data/tinyauth.db
|
||||
# Disable version heartbeat
|
||||
TINYAUTH_DISABLEANALYTICS=false
|
||||
# Disable static resource serving
|
||||
TINYAUTH_DISABLERESOURCES=false
|
||||
# Disable UI warning messages
|
||||
TINYAUTH_DISABLEUIWARNINGS=false
|
||||
|
||||
# Server Configuration
|
||||
|
||||
# Port to listen on
|
||||
TINYAUTH_SERVER_PORT=3000
|
||||
# Interface to bind to (0.0.0.0 for all interfaces)
|
||||
TINYAUTH_SERVER_ADDRESS=0.0.0.0
|
||||
# Unix socket path (optional, overrides port/address if set)
|
||||
TINYAUTH_SERVER_SOCKETPATH=
|
||||
# Comma-separated list of trusted proxy IPs/CIDRs
|
||||
TINYAUTH_SERVER_TRUSTEDPROXIES=
|
||||
|
||||
# Authentication Configuration
|
||||
|
||||
# Format: username:bcrypt_hash (use bcrypt to generate hash)
|
||||
TINYAUTH_AUTH_USERS=admin:$2a$10$example_bcrypt_hash_here
|
||||
# Path to external users file (optional)
|
||||
TINYAUTH_USERSFILE=
|
||||
# Enable secure cookies (requires HTTPS)
|
||||
TINYAUTH_SECURECOOKIE=true
|
||||
# Session expiry in seconds (7200 = 2 hours)
|
||||
TINYAUTH_SESSIONEXPIRY=7200
|
||||
# Login timeout in seconds (300 = 5 minutes)
|
||||
TINYAUTH_LOGINTIMEOUT=300
|
||||
# Maximum login retries before lockout
|
||||
TINYAUTH_LOGINMAXRETRIES=5
|
||||
|
||||
# OAuth Configuration
|
||||
|
||||
# Regex pattern for allowed email addresses (e.g., /@example\.com$/)
|
||||
TINYAUTH_OAUTH_WHITELIST=
|
||||
# Provider ID to auto-redirect to (skips login page)
|
||||
TINYAUTH_OAUTH_AUTOREDIRECT=
|
||||
# OAuth Provider Configuration (replace MYPROVIDER with your provider name)
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_CLIENTID=your_client_id_here
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_CLIENTSECRET=your_client_secret_here
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_AUTHURL=https://provider.example.com/oauth/authorize
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_TOKENURL=https://provider.example.com/oauth/token
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_USERINFOURL=https://provider.example.com/oauth/userinfo
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_REDIRECTURL=https://auth.example.com/oauth/callback/myprovider
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_SCOPES=openid email profile
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_NAME=My OAuth Provider
|
||||
# Allow self-signed certificates
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_INSECURE=false
|
||||
|
||||
# UI Customization
|
||||
|
||||
# Custom title for login page
|
||||
TINYAUTH_UI_TITLE=Tinyauth
|
||||
# Message shown on forgot password page
|
||||
TINYAUTH_UI_FORGOTPASSWORDMESSAGE="Contact your administrator to reset your password"
|
||||
# Background image URL for login page
|
||||
TINYAUTH_UI_BACKGROUNDIMAGE=
|
||||
|
||||
# LDAP Configuration
|
||||
|
||||
# LDAP server address
|
||||
TINYAUTH_LDAP_ADDRESS=ldap://ldap.example.com:389
|
||||
# DN for binding to LDAP server
|
||||
TINYAUTH_LDAP_BINDDN=cn=readonly,dc=example,dc=com
|
||||
# Password for bind DN
|
||||
TINYAUTH_LDAP_BINDPASSWORD=your_bind_password
|
||||
# Base DN for user searches
|
||||
TINYAUTH_LDAP_BASEDN=dc=example,dc=com
|
||||
# Search filter (%s will be replaced with username)
|
||||
TINYAUTH_LDAP_SEARCHFILTER=(&(uid=%s)(memberOf=cn=users,ou=groups,dc=example,dc=com))
|
||||
# Allow insecure LDAP connections
|
||||
TINYAUTH_LDAP_INSECURE=false
|
||||
PORT=3000
|
||||
ADDRESS=0.0.0.0
|
||||
APP_URL=http://localhost:3000
|
||||
USERS=your_user_password_hash
|
||||
USERS_FILE=users_file
|
||||
SECURE_COOKIE=false
|
||||
OAUTH_WHITELIST=
|
||||
GENERIC_NAME=My OAuth
|
||||
SESSION_EXPIRY=7200
|
||||
LOGIN_TIMEOUT=300
|
||||
LOGIN_MAX_RETRIES=5
|
||||
LOG_LEVEL=debug
|
||||
APP_TITLE=Tinyauth SSO
|
||||
FORGOT_PASSWORD_MESSAGE=Some message about resetting the password
|
||||
OAUTH_AUTO_REDIRECT=none
|
||||
BACKGROUND_IMAGE=some_image_url
|
||||
GENERIC_SKIP_SSL=false
|
||||
RESOURCES_DIR=/data/resources
|
||||
DATABASE_PATH=/data/tinyauth.db
|
||||
DISABLE_ANALYTICS=false
|
||||
DISABLE_RESOURCES=false
|
||||
TRUSTED_PROXIES=
|
||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -80,7 +80,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 cmd/*
|
||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 cmd/*
|
||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 cmd/*
|
||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 cmd/*
|
||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
app_url: "https://tinyauth.example.com"
|
||||
log_level: "info"
|
||||
resources_dir: "/etc/tinyauth/resources"
|
||||
database_path: "/var/lib/tinyauth/tinyauth.db"
|
||||
disable_analytics: false
|
||||
disable_resources: false
|
||||
disable_ui_warnings: false
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
address: "0.0.0.0"
|
||||
socket_path: "/var/run/tinyauth.sock"
|
||||
trusted_proxies: "10.10.10.0/24"
|
||||
|
||||
auth:
|
||||
users: "user:hash"
|
||||
users_file: "/etc/tinyauth/users.yaml"
|
||||
secure_cookie: true
|
||||
session_expiry: 3600
|
||||
login_timeout: 300
|
||||
login_max_retries: 5
|
||||
|
||||
oauth:
|
||||
whitelist: "example.com"
|
||||
auto_redirect: "pocketid"
|
||||
providers:
|
||||
google:
|
||||
client_id: "some-client-id"
|
||||
client_secret: "some-client-secret"
|
||||
client_secret_file: "some-client-secret-file"
|
||||
scopes:
|
||||
- "openid"
|
||||
- "email"
|
||||
- "profile"
|
||||
redirect_url: "https://tinyauth.example.com/oauth/callback/google"
|
||||
auth_url: "https://accounts.google.com/o/oauth2/auth"
|
||||
token_url: "https://oauth2.googleapis.com/token"
|
||||
user_info_url: "https://www.googleapis.com/oauth2/v3/userinfo"
|
||||
insecure: false
|
||||
name: "Google"
|
||||
|
||||
ui:
|
||||
title: "Tinyauth"
|
||||
forgot_password_message: "Contact your administrator to reset your password."
|
||||
background_image: "/static/background.jpg"
|
||||
|
||||
ldap:
|
||||
address: "ldap.example.com:389"
|
||||
base_dn: "dc=example,dc=com"
|
||||
bind_dn: "cn=admin,dc=example,dc=com"
|
||||
bind_password: "password"
|
||||
search_filter: "(uid={username})"
|
||||
insecure: false
|
||||
@@ -1,5 +1,5 @@
|
||||
# Site builder
|
||||
FROM oven/bun:1.3.4-alpine AS frontend-builder
|
||||
FROM oven/bun:1.3.3-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
@@ -33,14 +33,15 @@ COPY go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY ./main.go ./
|
||||
COPY ./cmd ./cmd
|
||||
COPY ./internal ./internal
|
||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" cmd/*
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}"
|
||||
|
||||
# Runner
|
||||
FROM alpine:3.23 AS runner
|
||||
FROM alpine:3.22 AS runner
|
||||
|
||||
WORKDIR /tinyauth
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ RUN go install github.com/go-delve/delve/cmd/dlv@latest
|
||||
|
||||
COPY ./cmd ./cmd
|
||||
COPY ./internal ./internal
|
||||
COPY ./main.go ./
|
||||
COPY ./air.toml ./
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Site builder
|
||||
FROM oven/bun:1.3.4-alpine AS frontend-builder
|
||||
FROM oven/bun:1.3.3-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
@@ -40,7 +40,7 @@ COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||
|
||||
RUN mkdir -p data
|
||||
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" cmd/*
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}"
|
||||
|
||||
# Runner
|
||||
FROM gcr.io/distroless/static-debian12:latest AS runner
|
||||
|
||||
2
air.toml
2
air.toml
@@ -3,7 +3,7 @@ tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
pre_cmd = ["mkdir -p internal/assets/dist", "mkdir -p /data", "echo 'backend running' > internal/assets/dist/index.html"]
|
||||
cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth cmd/*"
|
||||
cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ."
|
||||
bin = "tmp/tinyauth"
|
||||
full_bin = "dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false"
|
||||
include_ext = ["go"]
|
||||
|
||||
153
cmd/create.go
153
cmd/create.go
@@ -1,98 +1,99 @@
|
||||
package main
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/paerser/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type CreateUserConfig struct {
|
||||
Interactive bool `description:"Create a user interactively."`
|
||||
Docker bool `description:"Format output for docker."`
|
||||
Username string `description:"Username."`
|
||||
Password string `description:"Password."`
|
||||
type createUserCmd struct {
|
||||
root *cobra.Command
|
||||
cmd *cobra.Command
|
||||
|
||||
interactive bool
|
||||
docker bool
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func NewCreateUserConfig() *CreateUserConfig {
|
||||
return &CreateUserConfig{
|
||||
Interactive: false,
|
||||
Docker: false,
|
||||
Username: "",
|
||||
Password: "",
|
||||
func newCreateUserCmd(root *cobra.Command) *createUserCmd {
|
||||
return &createUserCmd{
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func createUserCmd() *cli.Command {
|
||||
tCfg := NewCreateUserConfig()
|
||||
|
||||
loaders := []cli.ResourceLoader{
|
||||
&cli.FlagLoader{},
|
||||
func (c *createUserCmd) Register() {
|
||||
c.cmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a user",
|
||||
Long: `Create a user either interactively or by passing flags.`,
|
||||
Run: c.run,
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Description: "Create a user",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
||||
c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Create a user interactively")
|
||||
c.cmd.Flags().BoolVar(&c.docker, "docker", false, "Format output for docker")
|
||||
c.cmd.Flags().StringVar(&c.username, "username", "", "Username")
|
||||
c.cmd.Flags().StringVar(&c.password, "password", "", "Password")
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
||||
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tCfg.Username == "" || tCfg.Password == "" {
|
||||
return errors.New("username and password cannot be empty")
|
||||
}
|
||||
|
||||
log.Info().Str("username", tCfg.Username).Msg("Creating user")
|
||||
|
||||
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// If docker format is enabled, escape the dollar sign
|
||||
passwdStr := string(passwd)
|
||||
if tCfg.Docker {
|
||||
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||
}
|
||||
|
||||
log.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
|
||||
|
||||
return nil
|
||||
},
|
||||
if c.root != nil {
|
||||
c.root.AddCommand(c.cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *createUserCmd) GetCmd() *cobra.Command {
|
||||
return c.cmd
|
||||
}
|
||||
|
||||
func (c *createUserCmd) run(cmd *cobra.Command, args []string) {
|
||||
log.Logger = log.Level(zerolog.InfoLevel)
|
||||
|
||||
if c.interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Username").Value(&c.username).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("username cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Password").Value(&c.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(&c.docker),
|
||||
),
|
||||
)
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Form failed")
|
||||
}
|
||||
}
|
||||
|
||||
if c.username == "" || c.password == "" {
|
||||
log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty")
|
||||
}
|
||||
|
||||
log.Info().Str("username", c.username).Msg("Creating user")
|
||||
|
||||
passwd, err := bcrypt.GenerateFromPassword([]byte(c.password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to hash password")
|
||||
}
|
||||
|
||||
// If docker format is enabled, escape the dollar sign
|
||||
passwdStr := string(passwd)
|
||||
if c.docker {
|
||||
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||
}
|
||||
|
||||
log.Info().Str("user", fmt.Sprintf("%s:%s", c.username, passwdStr)).Msg("User created")
|
||||
}
|
||||
|
||||
189
cmd/generate.go
189
cmd/generate.go
@@ -1,11 +1,10 @@
|
||||
package main
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
@@ -13,107 +12,109 @@ import (
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/paerser/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type GenerateTotpConfig struct {
|
||||
Interactive bool `description:"Generate a TOTP secret interactively."`
|
||||
User string `description:"Your current user (username:hash)."`
|
||||
type generateTotpCmd struct {
|
||||
root *cobra.Command
|
||||
cmd *cobra.Command
|
||||
|
||||
interactive bool
|
||||
user string
|
||||
}
|
||||
|
||||
func NewGenerateTotpConfig() *GenerateTotpConfig {
|
||||
return &GenerateTotpConfig{
|
||||
Interactive: false,
|
||||
User: "",
|
||||
func newGenerateTotpCmd(root *cobra.Command) *generateTotpCmd {
|
||||
return &generateTotpCmd{
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func generateTotpCmd() *cli.Command {
|
||||
tCfg := NewGenerateTotpConfig()
|
||||
|
||||
loaders := []cli.ResourceLoader{
|
||||
&cli.FlagLoader{},
|
||||
func (c *generateTotpCmd) Register() {
|
||||
c.cmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate a totp secret",
|
||||
Long: `Generate a totp secret for a user either interactively or by passing flags.`,
|
||||
Run: c.run,
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
Name: "generate",
|
||||
Description: "Generate a TOTP secret",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
||||
c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Run in interactive mode")
|
||||
c.cmd.Flags().StringVar(&c.user, "user", "", "Your current user (username:hash)")
|
||||
|
||||
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
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(tCfg.User)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse user: %w", err)
|
||||
}
|
||||
|
||||
docker := false
|
||||
if strings.Contains(tCfg.User, "$$") {
|
||||
docker = true
|
||||
}
|
||||
|
||||
if user.TotpSecret != "" {
|
||||
return fmt.Errorf("user already has a TOTP secret")
|
||||
}
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "Tinyauth",
|
||||
AccountName: user.Username,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate TOTP secret: %w", err)
|
||||
}
|
||||
|
||||
secret := key.Secret()
|
||||
|
||||
log.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
||||
|
||||
log.Info().Msg("Generated QR code")
|
||||
|
||||
config := qrterminal.Config{
|
||||
Level: qrterminal.L,
|
||||
Writer: os.Stdout,
|
||||
BlackChar: qrterminal.BLACK,
|
||||
WhiteChar: qrterminal.WHITE,
|
||||
QuietZone: 2,
|
||||
}
|
||||
|
||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||
|
||||
user.TotpSecret = secret
|
||||
|
||||
// If using docker escape re-escape it
|
||||
if docker {
|
||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||
}
|
||||
|
||||
log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
||||
|
||||
return nil
|
||||
},
|
||||
if c.root != nil {
|
||||
c.root.AddCommand(c.cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *generateTotpCmd) GetCmd() *cobra.Command {
|
||||
return c.cmd
|
||||
}
|
||||
|
||||
func (c *generateTotpCmd) run(cmd *cobra.Command, args []string) {
|
||||
log.Logger = log.Level(zerolog.InfoLevel)
|
||||
|
||||
if c.interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Current user (username:hash)").Value(&c.user).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("user cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
),
|
||||
)
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Form failed")
|
||||
}
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(c.user)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to parse user")
|
||||
}
|
||||
|
||||
docker := false
|
||||
if strings.Contains(c.user, "$$") {
|
||||
docker = true
|
||||
}
|
||||
|
||||
if user.TotpSecret != "" {
|
||||
log.Fatal().Msg("User already has a TOTP secret")
|
||||
}
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "Tinyauth",
|
||||
AccountName: user.Username,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to generate TOTP secret")
|
||||
}
|
||||
|
||||
secret := key.Secret()
|
||||
|
||||
log.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
||||
|
||||
log.Info().Msg("Generated QR code")
|
||||
|
||||
config := qrterminal.Config{
|
||||
Level: qrterminal.L,
|
||||
Writer: os.Stdout,
|
||||
BlackChar: qrterminal.BLACK,
|
||||
WhiteChar: qrterminal.WHITE,
|
||||
QuietZone: 2,
|
||||
}
|
||||
|
||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||
|
||||
user.TotpSecret = secret
|
||||
|
||||
// If using docker escape re-escape it
|
||||
if docker {
|
||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||
}
|
||||
|
||||
log.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.")
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
package main
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/paerser/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type healthzResponse struct {
|
||||
@@ -19,65 +17,96 @@ type healthzResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func healthcheckCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "healthcheck",
|
||||
Description: "Perform a health check",
|
||||
Configuration: nil,
|
||||
Resources: nil,
|
||||
AllowArg: true,
|
||||
Run: func(args []string) error {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
||||
type healthcheckCmd struct {
|
||||
root *cobra.Command
|
||||
cmd *cobra.Command
|
||||
|
||||
appUrl := os.Getenv("APPURL")
|
||||
viper *viper.Viper
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
appUrl = args[0]
|
||||
}
|
||||
|
||||
if appUrl == "" {
|
||||
return errors.New("APPURL is not set and no argument was provided")
|
||||
}
|
||||
|
||||
log.Info().Str("app_url", appUrl).Msg("Performing health check")
|
||||
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequest("GET", appUrl+"/api/healthz", nil)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to perform request: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("service is not healthy, got: %s", resp.Status)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var healthResp healthzResponse
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &healthResp)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Interface("response", healthResp).Msg("Tinyauth is healthy")
|
||||
|
||||
return nil
|
||||
},
|
||||
func newHealthcheckCmd(root *cobra.Command) *healthcheckCmd {
|
||||
return &healthcheckCmd{
|
||||
root: root,
|
||||
viper: viper.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *healthcheckCmd) Register() {
|
||||
c.cmd = &cobra.Command{
|
||||
Use: "healthcheck [app-url]",
|
||||
Short: "Perform a health check",
|
||||
Long: `Use the health check endpoint to verify that Tinyauth is running and it's healthy.`,
|
||||
Run: c.run,
|
||||
}
|
||||
|
||||
c.viper.AutomaticEnv()
|
||||
|
||||
if c.root != nil {
|
||||
c.root.AddCommand(c.cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *healthcheckCmd) GetCmd() *cobra.Command {
|
||||
return c.cmd
|
||||
}
|
||||
|
||||
func (c *healthcheckCmd) run(cmd *cobra.Command, args []string) {
|
||||
log.Logger = log.Level(zerolog.InfoLevel)
|
||||
|
||||
var appUrl string
|
||||
|
||||
port := c.viper.GetString("PORT")
|
||||
address := c.viper.GetString("ADDRESS")
|
||||
|
||||
if port == "" {
|
||||
port = "3000"
|
||||
}
|
||||
|
||||
if address == "" {
|
||||
address = "127.0.0.1"
|
||||
}
|
||||
|
||||
appUrl = "http://" + address + ":" + port
|
||||
|
||||
if len(args) > 0 {
|
||||
appUrl = args[0]
|
||||
}
|
||||
|
||||
log.Info().Str("app_url", appUrl).Msg("Performing health check")
|
||||
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequest("GET", appUrl+"/api/healthz", nil)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to create request")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to perform request")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Fatal().Err(errors.New("service is not healthy")).Msgf("Service is not healthy. Status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var healthResp healthzResponse
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to read response")
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &healthResp)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to decode response")
|
||||
}
|
||||
|
||||
log.Info().Interface("response", healthResp).Msg("Tinyauth is healthy")
|
||||
}
|
||||
|
||||
162
cmd/root.go
Normal file
162
cmd/root.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"tinyauth/internal/bootstrap"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type rootCmd struct {
|
||||
root *cobra.Command
|
||||
cmd *cobra.Command
|
||||
|
||||
viper *viper.Viper
|
||||
}
|
||||
|
||||
func newRootCmd() *rootCmd {
|
||||
return &rootCmd{
|
||||
viper: viper.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *rootCmd) Register() {
|
||||
c.cmd = &cobra.Command{
|
||||
Use: "tinyauth",
|
||||
Short: "The simplest way to protect your apps with a login screen",
|
||||
Long: `Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github or any other provider to all of your docker apps.`,
|
||||
Run: c.run,
|
||||
}
|
||||
|
||||
// Ignore unknown flags to allow --providers-*
|
||||
c.cmd.FParseErrWhitelist.UnknownFlags = true
|
||||
|
||||
c.viper.AutomaticEnv()
|
||||
|
||||
configOptions := []struct {
|
||||
name string
|
||||
defaultVal any
|
||||
description string
|
||||
}{
|
||||
{"port", 3000, "Port to run the server on."},
|
||||
{"address", "0.0.0.0", "Address to bind the server to."},
|
||||
{"app-url", "", "The Tinyauth URL."},
|
||||
{"users", "", "Comma separated list of users in the format username:hash."},
|
||||
{"users-file", "", "Path to a file containing users in the format username:hash."},
|
||||
{"secure-cookie", false, "Send cookie over secure connection only."},
|
||||
{"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."},
|
||||
{"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"},
|
||||
{"session-expiry", 86400, "Session (cookie) expiration time in seconds."},
|
||||
{"login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable)."},
|
||||
{"login-max-retries", 5, "Maximum login attempts before timeout (0 to disable)."},
|
||||
{"log-level", "info", "Log level."},
|
||||
{"app-title", "Tinyauth", "Title of the app."},
|
||||
{"forgot-password-message", "", "Message to show on the forgot password page."},
|
||||
{"background-image", "/background.jpg", "Background image URL for the login page."},
|
||||
{"ldap-address", "", "LDAP server address (e.g. ldap://localhost:389)."},
|
||||
{"ldap-bind-dn", "", "LDAP bind DN (e.g. uid=user,dc=example,dc=com)."},
|
||||
{"ldap-bind-password", "", "LDAP bind password."},
|
||||
{"ldap-base-dn", "", "LDAP base DN (e.g. dc=example,dc=com)."},
|
||||
{"ldap-insecure", false, "Skip certificate verification for the LDAP server."},
|
||||
{"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."},
|
||||
{"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."},
|
||||
{"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."},
|
||||
{"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."},
|
||||
{"disable-analytics", false, "Disable anonymous version collection."},
|
||||
{"disable-resources", false, "Disable the resources server."},
|
||||
{"socket-path", "", "Path to the Unix socket to bind the server to."},
|
||||
{"disable-ui-warnings", false, "Disable UI warnings about insecure configurations."},
|
||||
}
|
||||
|
||||
for _, opt := range configOptions {
|
||||
switch v := opt.defaultVal.(type) {
|
||||
case bool:
|
||||
c.cmd.Flags().Bool(opt.name, v, opt.description)
|
||||
case int:
|
||||
c.cmd.Flags().Int(opt.name, v, opt.description)
|
||||
case string:
|
||||
c.cmd.Flags().String(opt.name, v, opt.description)
|
||||
}
|
||||
|
||||
// Create uppercase env var name
|
||||
envVar := strings.ReplaceAll(strings.ToUpper(opt.name), "-", "_")
|
||||
c.viper.BindEnv(opt.name, envVar)
|
||||
}
|
||||
|
||||
c.viper.BindPFlags(c.cmd.Flags())
|
||||
|
||||
if c.root != nil {
|
||||
c.root.AddCommand(c.cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *rootCmd) GetCmd() *cobra.Command {
|
||||
return c.cmd
|
||||
}
|
||||
|
||||
func (c *rootCmd) run(cmd *cobra.Command, args []string) {
|
||||
var conf config.Config
|
||||
|
||||
err := c.viper.Unmarshal(&conf)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to parse config")
|
||||
}
|
||||
|
||||
v := validator.New()
|
||||
err = v.Struct(conf)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Invalid config")
|
||||
}
|
||||
|
||||
log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel)))
|
||||
log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting Tinyauth")
|
||||
|
||||
if log.Logger.GetLevel() == zerolog.TraceLevel {
|
||||
log.Warn().Msg("Log level set to trace, this will log sensitive information!")
|
||||
}
|
||||
|
||||
app := bootstrap.NewBootstrapApp(conf)
|
||||
|
||||
err = app.Setup()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to setup app")
|
||||
}
|
||||
}
|
||||
|
||||
func Run() {
|
||||
rootCmd := newRootCmd()
|
||||
rootCmd.Register()
|
||||
root := rootCmd.GetCmd()
|
||||
|
||||
userCmd := &cobra.Command{
|
||||
Use: "user",
|
||||
Short: "User utilities",
|
||||
Long: `Utilities for creating and verifying tinyauth compatible users.`,
|
||||
}
|
||||
totpCmd := &cobra.Command{
|
||||
Use: "totp",
|
||||
Short: "Totp utilities",
|
||||
Long: `Utilities for creating and verifying totp codes.`,
|
||||
}
|
||||
|
||||
newCreateUserCmd(userCmd).Register()
|
||||
newVerifyUserCmd(userCmd).Register()
|
||||
newGenerateTotpCmd(totpCmd).Register()
|
||||
newVersionCmd(root).Register()
|
||||
newHealthcheckCmd(root).Register()
|
||||
|
||||
root.AddCommand(userCmd)
|
||||
root.AddCommand(totpCmd)
|
||||
|
||||
err := root.Execute()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to execute root command")
|
||||
}
|
||||
}
|
||||
108
cmd/tinyauth.go
108
cmd/tinyauth.go
@@ -1,108 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"tinyauth/internal/bootstrap"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils/loaders"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/paerser/cli"
|
||||
)
|
||||
|
||||
type TinyauthCmdConfiguration struct {
|
||||
config.Config
|
||||
// ConfigFile string `description:"Path to config file."`
|
||||
}
|
||||
|
||||
func NewTinyauthCmdConfiguration() *TinyauthCmdConfiguration {
|
||||
return &TinyauthCmdConfiguration{
|
||||
Config: config.Config{
|
||||
LogLevel: "info",
|
||||
},
|
||||
// ConfigFile: "",
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
tConfig := NewTinyauthCmdConfiguration()
|
||||
|
||||
loaders := []cli.ResourceLoader{
|
||||
&loaders.EnvLoader{},
|
||||
&loaders.FlagLoader{},
|
||||
}
|
||||
|
||||
cmdTinyauth := &cli.Command{
|
||||
Name: "tinyauth",
|
||||
Description: "The simplest way to protect your apps with a login screen.",
|
||||
Configuration: tConfig,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
return runCmd(&tConfig.Config)
|
||||
},
|
||||
}
|
||||
|
||||
err := cmdTinyauth.AddCommand(versionCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add version command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(verifyUserCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add verify command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(healthcheckCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add healthcheck command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(generateTotpCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add generate command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(createUserCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
||||
}
|
||||
|
||||
err = cli.Execute(cmdTinyauth)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to execute command")
|
||||
}
|
||||
}
|
||||
|
||||
func runCmd(cfg *config.Config) error {
|
||||
logLevel, err := zerolog.ParseLevel(strings.ToLower(cfg.LogLevel))
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Invalid or missing log level, defaulting to info")
|
||||
} else {
|
||||
zerolog.SetGlobalLevel(logLevel)
|
||||
}
|
||||
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger()
|
||||
|
||||
log.Info().Str("version", config.Version).Msg("Starting tinyauth")
|
||||
|
||||
app := bootstrap.NewBootstrapApp(*cfg)
|
||||
|
||||
err = app.Setup()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to bootstrap app: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
194
cmd/verify.go
194
cmd/verify.go
@@ -1,120 +1,118 @@
|
||||
package main
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/paerser/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type VerifyUserConfig struct {
|
||||
Interactive bool `description:"Validate a user interactively."`
|
||||
Username string `description:"Username."`
|
||||
Password string `description:"Password."`
|
||||
Totp string `description:"TOTP code."`
|
||||
User string `description:"Hash (username:hash:totp)."`
|
||||
type verifyUserCmd struct {
|
||||
root *cobra.Command
|
||||
cmd *cobra.Command
|
||||
|
||||
interactive bool
|
||||
username string
|
||||
password string
|
||||
totp string
|
||||
user string
|
||||
}
|
||||
|
||||
func NewVerifyUserConfig() *VerifyUserConfig {
|
||||
return &VerifyUserConfig{
|
||||
Interactive: false,
|
||||
Username: "",
|
||||
Password: "",
|
||||
Totp: "",
|
||||
User: "",
|
||||
func newVerifyUserCmd(root *cobra.Command) *verifyUserCmd {
|
||||
return &verifyUserCmd{
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func verifyUserCmd() *cli.Command {
|
||||
tCfg := NewVerifyUserConfig()
|
||||
|
||||
loaders := []cli.ResourceLoader{
|
||||
&cli.FlagLoader{},
|
||||
func (c *verifyUserCmd) Register() {
|
||||
c.cmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify a user is set up correctly",
|
||||
Long: `Verify a user is set up correctly meaning that it has a correct username, password and TOTP code.`,
|
||||
Run: c.run,
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
Name: "verify",
|
||||
Description: "Verify a user is set up correctly.",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
||||
c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Validate a user interactively")
|
||||
c.cmd.Flags().StringVar(&c.username, "username", "", "Username")
|
||||
c.cmd.Flags().StringVar(&c.password, "password", "", "Password")
|
||||
c.cmd.Flags().StringVar(&c.totp, "totp", "", "TOTP code")
|
||||
c.cmd.Flags().StringVar(&c.user, "user", "", "Hash (username:hash: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),
|
||||
),
|
||||
)
|
||||
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(tCfg.User)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse user: %w", err)
|
||||
}
|
||||
|
||||
if user.Username != tCfg.Username {
|
||||
return fmt.Errorf("username is incorrect")
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(tCfg.Password))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("password is incorrect: %w", err)
|
||||
}
|
||||
|
||||
if user.TotpSecret == "" {
|
||||
if tCfg.Totp != "" {
|
||||
log.Warn().Msg("User does not have TOTP secret")
|
||||
}
|
||||
log.Info().Msg("User verified")
|
||||
return nil
|
||||
}
|
||||
|
||||
ok := totp.Validate(tCfg.Totp, user.TotpSecret)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("TOTP code incorrect: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Msg("User verified")
|
||||
|
||||
return nil
|
||||
},
|
||||
if c.root != nil {
|
||||
c.root.AddCommand(c.cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *verifyUserCmd) GetCmd() *cobra.Command {
|
||||
return c.cmd
|
||||
}
|
||||
|
||||
func (c *verifyUserCmd) run(cmd *cobra.Command, args []string) {
|
||||
log.Logger = log.Level(zerolog.InfoLevel)
|
||||
|
||||
if c.interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("User (username:hash:totp)").Value(&c.user).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("user cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Username").Value(&c.username).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("username cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Password").Value(&c.password).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("password cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("TOTP Code (optional)").Value(&c.totp),
|
||||
),
|
||||
)
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Form failed")
|
||||
}
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(c.user)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to parse user")
|
||||
}
|
||||
|
||||
if user.Username != c.username {
|
||||
log.Fatal().Msg("Username is incorrect")
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(c.password))
|
||||
if err != nil {
|
||||
log.Fatal().Msg("Password is incorrect")
|
||||
}
|
||||
|
||||
if user.TotpSecret == "" {
|
||||
if c.totp != "" {
|
||||
log.Warn().Msg("User does not have TOTP secret")
|
||||
}
|
||||
log.Info().Msg("User verified")
|
||||
return
|
||||
}
|
||||
|
||||
ok := totp.Validate(c.totp, user.TotpSecret)
|
||||
if !ok {
|
||||
log.Fatal().Msg("TOTP code incorrect")
|
||||
|
||||
}
|
||||
|
||||
log.Info().Msg("User verified")
|
||||
}
|
||||
|
||||
@@ -1,23 +1,42 @@
|
||||
package main
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"github.com/traefik/paerser/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func versionCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "version",
|
||||
Description: "Print the version number of Tinyauth.",
|
||||
Configuration: nil,
|
||||
Resources: nil,
|
||||
Run: func(_ []string) error {
|
||||
fmt.Printf("Version: %s\n", config.Version)
|
||||
fmt.Printf("Commit Hash: %s\n", config.CommitHash)
|
||||
fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp)
|
||||
return nil
|
||||
},
|
||||
type versionCmd struct {
|
||||
root *cobra.Command
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newVersionCmd(root *cobra.Command) *versionCmd {
|
||||
return &versionCmd{
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *versionCmd) Register() {
|
||||
c.cmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number of Tinyauth",
|
||||
Long: `All software has versions. This is Tinyauth's.`,
|
||||
Run: c.run,
|
||||
}
|
||||
|
||||
if c.root != nil {
|
||||
c.root.AddCommand(c.cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *versionCmd) GetCmd() *cobra.Command {
|
||||
return c.cmd
|
||||
}
|
||||
|
||||
func (c *versionCmd) run(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Version: %s\n", config.Version)
|
||||
fmt.Printf("Commit Hash: %s\n", config.CommitHash)
|
||||
fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ services:
|
||||
container_name: tinyauth
|
||||
image: ghcr.io/steveiliop56/tinyauth:v3
|
||||
environment:
|
||||
- TINYAUTH_APPURL=https://tinyauth.example.com
|
||||
- TINYAUTH_AUTH_USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||
- APP_URL=https://tinyauth.example.com
|
||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||
volumes:
|
||||
- ./data:/data
|
||||
labels:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "tinyauth-shadcn",
|
||||
@@ -12,22 +11,22 @@
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@tanstack/react-query": "^5.90.11",
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^25.7.2",
|
||||
"i18next": "^25.7.1",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.556.0",
|
||||
"lucide-react": "^0.555.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-i18next": "^16.4.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.67.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.10.1",
|
||||
"react-router": "^7.10.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
@@ -36,10 +35,10 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||
"@types/node": "^24.10.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
@@ -47,8 +46,8 @@
|
||||
"prettier": "3.7.4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.49.0",
|
||||
"vite": "^7.2.7",
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"vite": "^7.2.6",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -261,7 +260,7 @@
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="],
|
||||
|
||||
@@ -337,9 +336,9 @@
|
||||
|
||||
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="],
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.11", "", {}, "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="],
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.11", "", { "dependencies": { "@tanstack/query-core": "5.90.11" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
@@ -363,7 +362,7 @@
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="],
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||
|
||||
@@ -371,29 +370,29 @@
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.49.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/type-utils": "8.49.0", "@typescript-eslint/utils": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.49.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.48.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/type-utils": "8.48.1", "@typescript-eslint/utils": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.49.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.48.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.49.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.49.0", "@typescript-eslint/types": "^8.49.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g=="],
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.48.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.48.1", "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.49.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA=="],
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.48.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0", "@typescript-eslint/utils": "8.49.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.49.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.49.0", "@typescript-eslint/tsconfig-utils": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.48.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.48.1", "@typescript-eslint/tsconfig-utils": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA=="],
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="],
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
@@ -567,6 +566,8 @@
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
@@ -587,7 +588,7 @@
|
||||
|
||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||
|
||||
"i18next": ["i18next@25.7.2", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-58b4kmLpLv1buWUEwegMDUqZVR5J+rT+WTRFaBGL7lxDuJQQ0NrJFrq+eT2N94aYVR1k1Sr13QITNOL88tZCuw=="],
|
||||
"i18next": ["i18next@25.7.1", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-XbTnkh1yCZWSAZGnA9xcQfHcYNgZs2cNxm+c6v1Ma9UAUGCeJPplRe1ILia6xnDvXBjk0uXU+Z8FYWhA19SKFw=="],
|
||||
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
||||
|
||||
@@ -673,7 +674,7 @@
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.556.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A=="],
|
||||
"lucide-react": ["lucide-react@0.555.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
@@ -789,13 +790,13 @@
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="],
|
||||
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
|
||||
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.68.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q=="],
|
||||
"react-hook-form": ["react-hook-form@7.67.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ=="],
|
||||
|
||||
"react-i18next": ["react-i18next@16.4.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-bxVeBA8Ky2UeItNhF4JRxHCFIrpEJHGFG/mOAa4CR0JkqaDEYSLmlEgmC4Os63SBlZ+E5U0YyrNJOSVl2mtVqQ=="],
|
||||
"react-i18next": ["react-i18next@16.3.5", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-F7Kglc+T0aE6W2rO5eCAFBEuWRpNb5IFmXOYEgztjZEuiuSLTe/xBIEG6Q3S0fbl8GXMNo+Q7gF8bpokFNWJww=="],
|
||||
|
||||
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
||||
|
||||
@@ -805,7 +806,7 @@
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
|
||||
"react-router": ["react-router@7.10.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw=="],
|
||||
"react-router": ["react-router@7.10.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw=="],
|
||||
|
||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
@@ -871,7 +872,7 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.49.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.49.0", "@typescript-eslint/parser": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0", "@typescript-eslint/utils": "8.49.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.48.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.48.1", "@typescript-eslint/parser": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -901,7 +902,7 @@
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
|
||||
|
||||
"vite": ["vite@7.2.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ=="],
|
||||
"vite": ["vite@7.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ=="],
|
||||
|
||||
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
|
||||
|
||||
@@ -993,25 +994,25 @@
|
||||
|
||||
"@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0" } }, "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1" } }, "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.49.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.48.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0" } }, "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg=="],
|
||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1" } }, "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="],
|
||||
|
||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="],
|
||||
|
||||
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.49.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.48.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
@@ -1019,7 +1020,7 @@
|
||||
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.1", "@typescript-eslint/tsconfig-utils": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
||||
|
||||
@@ -1037,7 +1038,7 @@
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.49.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA=="],
|
||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.48.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="],
|
||||
|
||||
@@ -1059,11 +1060,11 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0" } }, "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1" } }, "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
@@ -1083,9 +1084,9 @@
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0" } }, "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg=="],
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1" } }, "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
|
||||
@@ -18,22 +18,22 @@
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@tanstack/react-query": "^5.90.11",
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^25.7.2",
|
||||
"i18next": "^25.7.1",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.556.0",
|
||||
"lucide-react": "^0.555.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-i18next": "^16.4.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.67.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.10.1",
|
||||
"react-router": "^7.10.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
@@ -42,10 +42,10 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||
"@types/node": "^24.10.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
@@ -53,7 +53,7 @@
|
||||
"prettier": "3.7.4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.49.0",
|
||||
"vite": "^7.2.7"
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"vite": "^7.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ i18n
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
debug: import.meta.env.MODE === "development",
|
||||
nonExplicitSupportedLngs: true,
|
||||
load: "currentOnly",
|
||||
detection: {
|
||||
lookupLocalStorage: "tinyauth-lang",
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
load: "currentOnly",
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"loginTitle": "Welcome back, login with",
|
||||
"loginTitleSimple": "Welcome back, please login",
|
||||
"loginDivider": "Or",
|
||||
"loginUsername": "Username",
|
||||
"loginPassword": "Password",
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginTitleSimple": "Üdvözöljük, kérem jelentkezzen be",
|
||||
"loginDivider": "Vagy",
|
||||
"loginUsername": "Felhasználónév",
|
||||
"loginPassword": "Jelszó",
|
||||
"loginSubmit": "Bejelentkezés",
|
||||
"loginFailTitle": "Sikertelen bejelentkezés",
|
||||
"loginFailSubtitle": "Kérjük, ellenőrizze a felhasználónevét és jelszavát",
|
||||
"loginFailRateLimit": "Túl sokszor próbálkoztál bejelentkezni. Próbáld újra később",
|
||||
"loginSuccessTitle": "Bejelentkezve",
|
||||
"loginSuccessSubtitle": "Üdvözöljük!",
|
||||
"loginOauthFailTitle": "An error occurred",
|
||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
||||
"loginOauthSuccessTitle": "Redirecting",
|
||||
"loginOauthSuccessTitle": "Átirányítás",
|
||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
||||
"loginOauthAutoRedirectButton": "Redirect now",
|
||||
"continueTitle": "Continue",
|
||||
"continueRedirectingTitle": "Redirecting...",
|
||||
"continueRedirectingTitle": "Átirányítás...",
|
||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||
"continueRedirectManually": "Redirect me manually",
|
||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
||||
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
|
||||
"logoutFailTitle": "Failed to log out",
|
||||
"logoutFailSubtitle": "Please try again",
|
||||
"logoutSuccessTitle": "Logged out",
|
||||
"logoutSuccessSubtitle": "You have been logged out",
|
||||
"logoutTitle": "Logout",
|
||||
"logoutFailTitle": "Sikertelen kijelentkezés",
|
||||
"logoutFailSubtitle": "Próbálja újra",
|
||||
"logoutSuccessTitle": "Kijelentkezve",
|
||||
"logoutSuccessSubtitle": "Kijelentkeztél",
|
||||
"logoutTitle": "Kijelentkezés",
|
||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
||||
"notFoundTitle": "Page not found",
|
||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
||||
"notFoundButton": "Go home",
|
||||
"totpFailTitle": "Failed to verify code",
|
||||
"totpFailSubtitle": "Please check your code and try again",
|
||||
"notFoundButton": "Ugrás a kezdőlapra",
|
||||
"totpFailTitle": "Érvénytelen kód",
|
||||
"totpFailSubtitle": "Kérjük ellenőrizze a kódot és próbálja újra",
|
||||
"totpSuccessTitle": "Verified",
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
@@ -46,14 +46,14 @@
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"cancelTitle": "Cancel",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"unauthorizedButton": "Próbálja újra",
|
||||
"cancelTitle": "Mégse",
|
||||
"forgotPasswordTitle": "Elfelejtette jelszavát?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorTitle": "Hiba történt",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"fieldRequired": "Ez egy kötelező mező",
|
||||
"invalidInput": "Invalid input",
|
||||
"domainWarningTitle": "Invalid Domain",
|
||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"loginTitle": "Welcome back, login with",
|
||||
"loginTitleSimple": "Welcome back, please login",
|
||||
"loginDivider": "Or",
|
||||
"loginUsername": "Username",
|
||||
"loginTitle": "Bentornato, accedi con",
|
||||
"loginTitleSimple": "Bentornato, accedi al tuo account",
|
||||
"loginDivider": "Oppure",
|
||||
"loginUsername": "Nome utente",
|
||||
"loginPassword": "Password",
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "An error occurred",
|
||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
||||
"loginSubmit": "Accesso",
|
||||
"loginFailTitle": "Accesso non riuscito",
|
||||
"loginFailSubtitle": "Verifica che il nome utente e la password siano corretti",
|
||||
"loginFailRateLimit": "Hai effettuato troppi tentativi errati. Riprova più tardi",
|
||||
"loginSuccessTitle": "Accesso effettuato",
|
||||
"loginSuccessSubtitle": "Bentornato!",
|
||||
"loginOauthFailTitle": "Si è verificato un errore",
|
||||
"loginOauthFailSubtitle": "Impossibile ottenere l'URL di OAuth",
|
||||
"loginOauthSuccessTitle": "Redirecting",
|
||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
||||
"loginOauthAutoRedirectButton": "Redirect now",
|
||||
"continueTitle": "Continue",
|
||||
"continueTitle": "Prosegui",
|
||||
"continueRedirectingTitle": "Redirecting...",
|
||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||
"continueRedirectManually": "Redirect me manually",
|
||||
@@ -34,29 +34,29 @@
|
||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
||||
"notFoundTitle": "Page not found",
|
||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
||||
"notFoundButton": "Go home",
|
||||
"totpFailTitle": "Failed to verify code",
|
||||
"totpFailSubtitle": "Please check your code and try again",
|
||||
"totpSuccessTitle": "Verified",
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Try again",
|
||||
"cancelTitle": "Cancel",
|
||||
"forgotPasswordTitle": "Forgot your password?",
|
||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||
"errorTitle": "An error occurred",
|
||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||
"fieldRequired": "This field is required",
|
||||
"invalidInput": "Invalid input",
|
||||
"domainWarningTitle": "Invalid Domain",
|
||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||
"ignoreTitle": "Ignore",
|
||||
"goToCorrectDomainTitle": "Go to correct domain"
|
||||
"notFoundButton": "Vai alla home",
|
||||
"totpFailTitle": "Errore nella verifica del codice",
|
||||
"totpFailSubtitle": "Si prega di controllare il codice e riprovare",
|
||||
"totpSuccessTitle": "Verificato",
|
||||
"totpSuccessSubtitle": "Reindirizzamento alla tua app",
|
||||
"totpTitle": "Inserisci il tuo codice TOTP",
|
||||
"totpSubtitle": "Inserisci il codice dalla tua app di autenticazione.",
|
||||
"unauthorizedTitle": "Non Autorizzato",
|
||||
"unauthorizedResourceSubtitle": "L'utente con username <code>{{username}}</code> non è autorizzato ad accedere alla risorsa <code>{{resource}}</code>.",
|
||||
"unauthorizedLoginSubtitle": "L'utente con username <code>{{username}}</code> non è autorizzato a effettuare l'accesso.",
|
||||
"unauthorizedGroupsSubtitle": "L'utente con nome utente <code>{{username}}</code> non fa parte dei gruppi richiesti dalla risorsa <code>{{resource}}</code>.",
|
||||
"unauthorizedIpSubtitle": "Il tuo indirizzo IP <code>{{ip}}</code> non è autorizzato ad accedere alla risorsa <code>{{resource}}</code>.",
|
||||
"unauthorizedButton": "Riprova",
|
||||
"cancelTitle": "Annulla",
|
||||
"forgotPasswordTitle": "Password dimenticata?",
|
||||
"failedToFetchProvidersTitle": "Impossibile caricare i provider di autenticazione. Si prega di controllare la configurazione.",
|
||||
"errorTitle": "Si è verificato un errore",
|
||||
"errorSubtitle": "Si è verificato un errore durante il tentativo di eseguire questa azione. Si prega di controllare la console per ulteriori informazioni.",
|
||||
"forgotPasswordMessage": "Puoi reimpostare la tua password modificando la variabile d'ambiente `USERS`.",
|
||||
"fieldRequired": "Questo campo è obbligatorio",
|
||||
"invalidInput": "Input non valido",
|
||||
"domainWarningTitle": "Dominio non valido",
|
||||
"domainWarningSubtitle": "Questa istanza è configurata per essere accessibile da <code>{{appUrl}}</code>, ma <code>{{currentUrl}}</code> è in uso. Se procedi, potresti incorrere in problemi di autenticazione.",
|
||||
"ignoreTitle": "Ignora",
|
||||
"goToCorrectDomainTitle": "Vai al dominio corretto"
|
||||
}
|
||||
63
go.mod
63
go.mod
@@ -8,15 +8,18 @@ require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
github.com/google/go-querystring v1.1.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mdp/qrterminal/v3 v3.2.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stoewer/go-strcase v1.3.1
|
||||
github.com/traefik/paerser v0.2.2
|
||||
github.com/weppos/publicsuffix-go v0.50.1
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
gorm.io/gorm v1.31.1
|
||||
gotest.tools/v3 v3.5.2
|
||||
@@ -24,10 +27,6 @@ require (
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
@@ -35,33 +34,26 @@ require (
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
@@ -92,6 +84,7 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
@@ -100,7 +93,8 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // 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.2.0 // indirect
|
||||
@@ -111,28 +105,33 @@ require (
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
golang.org/x/oauth2 v0.33.0
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
)
|
||||
|
||||
196
go.sum
196
go.sum
@@ -2,17 +2,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
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=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
@@ -32,6 +23,8 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
||||
@@ -71,6 +64,7 @@ github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmC
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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=
|
||||
@@ -93,6 +87,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
@@ -120,6 +116,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
@@ -135,19 +133,14 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
@@ -164,8 +157,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -193,14 +186,8 @@ github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuE
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
@@ -212,9 +199,8 @@ github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFL
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
@@ -227,8 +213,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
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=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -238,10 +224,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
|
||||
github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -252,27 +238,37 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
|
||||
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ=
|
||||
github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
@@ -283,99 +279,71 @@ github.com/weppos/publicsuffix-go v0.50.1 h1:elrBHeSkS/eIb169+DnLrknqmdP4AjT0Q0t
|
||||
github.com/weppos/publicsuffix-go v0.50.1/go.mod h1:znn0JVXjcR5hpUl9pbEogwH6I710rA1AX0QQPT0bf+k=
|
||||
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.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
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.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -13,26 +13,32 @@ import (
|
||||
"time"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/controller"
|
||||
"tinyauth/internal/middleware"
|
||||
"tinyauth/internal/model"
|
||||
"tinyauth/internal/service"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Controller interface {
|
||||
SetupRoutes()
|
||||
}
|
||||
|
||||
type Middleware interface {
|
||||
Middleware() gin.HandlerFunc
|
||||
Init() error
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
Init() error
|
||||
}
|
||||
|
||||
type BootstrapApp struct {
|
||||
config config.Config
|
||||
context struct {
|
||||
uuid string
|
||||
cookieDomain string
|
||||
sessionCookieName string
|
||||
csrfCookieName string
|
||||
redirectCookieName string
|
||||
users []config.User
|
||||
oauthProviders map[string]config.OAuthServiceConfig
|
||||
configuredProviders []controller.Provider
|
||||
}
|
||||
services Services
|
||||
config config.Config
|
||||
uuid string
|
||||
}
|
||||
|
||||
func NewBootstrapApp(config config.Config) *BootstrapApp {
|
||||
@@ -43,42 +49,17 @@ func NewBootstrapApp(config config.Config) *BootstrapApp {
|
||||
|
||||
func (app *BootstrapApp) Setup() error {
|
||||
// Parse users
|
||||
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
||||
users, err := utils.GetUsers(app.config.Users, app.config.UsersFile)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app.context.users = users
|
||||
// Get OAuth configs
|
||||
oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.config.AppURL)
|
||||
|
||||
// Setup OAuth providers
|
||||
app.context.oauthProviders = app.config.OAuth.Providers
|
||||
|
||||
for name, provider := range app.context.oauthProviders {
|
||||
secret := utils.GetSecret(provider.ClientSecret, provider.ClientSecretFile)
|
||||
provider.ClientSecret = secret
|
||||
provider.ClientSecretFile = ""
|
||||
app.context.oauthProviders[name] = provider
|
||||
}
|
||||
|
||||
for id := range config.OverrideProviders {
|
||||
if provider, exists := app.context.oauthProviders[id]; exists {
|
||||
if provider.RedirectURL == "" {
|
||||
provider.RedirectURL = app.config.AppURL + "/api/oauth/callback/" + id
|
||||
app.context.oauthProviders[id] = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id, provider := range app.context.oauthProviders {
|
||||
if provider.Name == "" {
|
||||
if name, ok := config.OverrideProviders[id]; ok {
|
||||
provider.Name = name
|
||||
} else {
|
||||
provider.Name = utils.Capitalize(id)
|
||||
}
|
||||
}
|
||||
app.context.oauthProviders[id] = provider
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get cookie domain
|
||||
@@ -88,38 +69,102 @@ func (app *BootstrapApp) Setup() error {
|
||||
return err
|
||||
}
|
||||
|
||||
app.context.cookieDomain = cookieDomain
|
||||
|
||||
// Cookie names
|
||||
appUrl, _ := url.Parse(app.config.AppURL) // Already validated
|
||||
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
||||
cookieId := strings.Split(app.context.uuid, "-")[0]
|
||||
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
||||
app.context.csrfCookieName = fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
|
||||
app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
||||
uuid := utils.GenerateUUID(appUrl.Hostname())
|
||||
app.uuid = uuid
|
||||
cookieId := strings.Split(uuid, "-")[0]
|
||||
sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
||||
csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
|
||||
redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
||||
|
||||
// Dumps
|
||||
log.Trace().Interface("config", app.config).Msg("Config dump")
|
||||
log.Trace().Interface("users", app.context.users).Msg("Users dump")
|
||||
log.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
||||
log.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
||||
log.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
||||
log.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
|
||||
log.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
|
||||
log.Trace().Interface("users", users).Msg("Users dump")
|
||||
log.Trace().Interface("oauthProviders", oauthProviders).Msg("OAuth providers dump")
|
||||
log.Trace().Str("cookieDomain", cookieDomain).Msg("Cookie domain")
|
||||
log.Trace().Str("sessionCookieName", sessionCookieName).Msg("Session cookie name")
|
||||
log.Trace().Str("csrfCookieName", csrfCookieName).Msg("CSRF cookie name")
|
||||
log.Trace().Str("redirectCookieName", redirectCookieName).Msg("Redirect cookie name")
|
||||
|
||||
// Services
|
||||
services, err := app.initServices()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize services: %w", err)
|
||||
// Create configs
|
||||
authConfig := service.AuthServiceConfig{
|
||||
Users: users,
|
||||
OauthWhitelist: app.config.OAuthWhitelist,
|
||||
SessionExpiry: app.config.SessionExpiry,
|
||||
SecureCookie: app.config.SecureCookie,
|
||||
CookieDomain: cookieDomain,
|
||||
LoginTimeout: app.config.LoginTimeout,
|
||||
LoginMaxRetries: app.config.LoginMaxRetries,
|
||||
SessionCookieName: sessionCookieName,
|
||||
}
|
||||
|
||||
app.services = services
|
||||
// Setup services
|
||||
var ldapService *service.LdapService
|
||||
|
||||
if app.config.LdapAddress != "" {
|
||||
ldapConfig := service.LdapServiceConfig{
|
||||
Address: app.config.LdapAddress,
|
||||
BindDN: app.config.LdapBindDN,
|
||||
BindPassword: app.config.LdapBindPassword,
|
||||
BaseDN: app.config.LdapBaseDN,
|
||||
Insecure: app.config.LdapInsecure,
|
||||
SearchFilter: app.config.LdapSearchFilter,
|
||||
}
|
||||
|
||||
ldapService = service.NewLdapService(ldapConfig)
|
||||
|
||||
err := ldapService.Init()
|
||||
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without LDAP")
|
||||
ldapService = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap database
|
||||
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
|
||||
DatabasePath: app.config.DatabasePath,
|
||||
})
|
||||
|
||||
log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service")
|
||||
|
||||
err = databaseService.Init()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize database service: %w", err)
|
||||
}
|
||||
|
||||
database := databaseService.GetDatabase()
|
||||
|
||||
// Create services
|
||||
dockerService := service.NewDockerService()
|
||||
aclsService := service.NewAccessControlsService(dockerService)
|
||||
authService := service.NewAuthService(authConfig, dockerService, ldapService, database)
|
||||
oauthBrokerService := service.NewOAuthBrokerService(oauthProviders)
|
||||
|
||||
// Initialize services (order matters)
|
||||
services := []Service{
|
||||
dockerService,
|
||||
aclsService,
|
||||
authService,
|
||||
oauthBrokerService,
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
if svc != nil {
|
||||
log.Debug().Str("service", fmt.Sprintf("%T", svc)).Msg("Initializing service")
|
||||
err := svc.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configured providers
|
||||
configuredProviders := make([]controller.Provider, 0)
|
||||
|
||||
for id, provider := range app.context.oauthProviders {
|
||||
for id, provider := range oauthProviders {
|
||||
configuredProviders = append(configuredProviders, controller.Provider{
|
||||
Name: provider.Name,
|
||||
ID: id,
|
||||
@@ -131,7 +176,7 @@ func (app *BootstrapApp) Setup() error {
|
||||
return configuredProviders[i].Name < configuredProviders[j].Name
|
||||
})
|
||||
|
||||
if services.authService.UserAuthConfigured() {
|
||||
if authService.UserAuthConfigured() || ldapService != nil {
|
||||
configuredProviders = append(configuredProviders, controller.Provider{
|
||||
Name: "Username",
|
||||
ID: "username",
|
||||
@@ -145,18 +190,92 @@ func (app *BootstrapApp) Setup() error {
|
||||
return fmt.Errorf("no authentication providers configured")
|
||||
}
|
||||
|
||||
app.context.configuredProviders = configuredProviders
|
||||
// Create engine
|
||||
engine := gin.New()
|
||||
engine.Use(gin.Recovery())
|
||||
|
||||
// Setup router
|
||||
router, err := app.setupRouter()
|
||||
if len(app.config.TrustedProxies) > 0 {
|
||||
err := engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ","))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup routes: %w", err)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set trusted proxies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start DB cleanup routine
|
||||
log.Debug().Msg("Starting database cleanup routine")
|
||||
go app.dbCleanup(services.databaseService.GetDatabase())
|
||||
// Create middlewares
|
||||
var middlewares []Middleware
|
||||
|
||||
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
|
||||
CookieDomain: cookieDomain,
|
||||
}, authService, oauthBrokerService)
|
||||
|
||||
uiMiddleware := middleware.NewUIMiddleware()
|
||||
zerologMiddleware := middleware.NewZerologMiddleware()
|
||||
|
||||
middlewares = append(middlewares, contextMiddleware, uiMiddleware, zerologMiddleware)
|
||||
|
||||
for _, middleware := range middlewares {
|
||||
log.Debug().Str("middleware", fmt.Sprintf("%T", middleware)).Msg("Initializing middleware")
|
||||
err := middleware.Init()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize middleware %T: %w", middleware, err)
|
||||
}
|
||||
engine.Use(middleware.Middleware())
|
||||
}
|
||||
|
||||
// Create routers
|
||||
mainRouter := engine.Group("")
|
||||
apiRouter := engine.Group("/api")
|
||||
|
||||
// Create controllers
|
||||
contextController := controller.NewContextController(controller.ContextControllerConfig{
|
||||
Providers: configuredProviders,
|
||||
Title: app.config.Title,
|
||||
AppURL: app.config.AppURL,
|
||||
CookieDomain: cookieDomain,
|
||||
ForgotPasswordMessage: app.config.ForgotPasswordMessage,
|
||||
BackgroundImage: app.config.BackgroundImage,
|
||||
OAuthAutoRedirect: app.config.OAuthAutoRedirect,
|
||||
DisableUIWarnings: app.config.DisableUIWarnings,
|
||||
}, apiRouter)
|
||||
|
||||
oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{
|
||||
AppURL: app.config.AppURL,
|
||||
SecureCookie: app.config.SecureCookie,
|
||||
CSRFCookieName: csrfCookieName,
|
||||
RedirectCookieName: redirectCookieName,
|
||||
CookieDomain: cookieDomain,
|
||||
}, apiRouter, authService, oauthBrokerService)
|
||||
|
||||
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
|
||||
AppURL: app.config.AppURL,
|
||||
}, apiRouter, aclsService, authService)
|
||||
|
||||
userController := controller.NewUserController(controller.UserControllerConfig{
|
||||
CookieDomain: cookieDomain,
|
||||
}, apiRouter, authService)
|
||||
|
||||
resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{
|
||||
ResourcesDir: app.config.ResourcesDir,
|
||||
ResourcesDisabled: app.config.DisableResources,
|
||||
}, mainRouter)
|
||||
|
||||
healthController := controller.NewHealthController(apiRouter)
|
||||
|
||||
// Setup routes
|
||||
controller := []Controller{
|
||||
contextController,
|
||||
oauthController,
|
||||
proxyController,
|
||||
userController,
|
||||
healthController,
|
||||
resourcesController,
|
||||
}
|
||||
|
||||
for _, ctrl := range controller {
|
||||
log.Debug().Msgf("Setting up %T controller", ctrl)
|
||||
ctrl.SetupRoutes()
|
||||
}
|
||||
|
||||
// If analytics are not disabled, start heartbeat
|
||||
if !app.config.DisableAnalytics {
|
||||
@@ -164,18 +283,24 @@ func (app *BootstrapApp) Setup() error {
|
||||
go app.heartbeat()
|
||||
}
|
||||
|
||||
// Start DB cleanup routine
|
||||
log.Debug().Msg("Starting database cleanup routine")
|
||||
go app.dbCleanup(database)
|
||||
|
||||
// If we have an socket path, bind to it
|
||||
if app.config.Server.SocketPath != "" {
|
||||
if _, err := os.Stat(app.config.Server.SocketPath); err == nil {
|
||||
log.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
|
||||
err := os.Remove(app.config.Server.SocketPath)
|
||||
if app.config.SocketPath != "" {
|
||||
// Remove existing socket file
|
||||
if _, err := os.Stat(app.config.SocketPath); err == nil {
|
||||
log.Info().Msgf("Removing existing socket file %s", app.config.SocketPath)
|
||||
err := os.Remove(app.config.SocketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove existing socket file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
|
||||
if err := router.RunUnix(app.config.Server.SocketPath); err != nil {
|
||||
// Start server with unix socket
|
||||
log.Info().Msgf("Starting server on unix socket %s", app.config.SocketPath)
|
||||
if err := engine.RunUnix(app.config.SocketPath); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start server")
|
||||
}
|
||||
|
||||
@@ -183,9 +308,9 @@ func (app *BootstrapApp) Setup() error {
|
||||
}
|
||||
|
||||
// Start server
|
||||
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
||||
address := fmt.Sprintf("%s:%d", app.config.Address, app.config.Port)
|
||||
log.Info().Msgf("Starting server on %s", address)
|
||||
if err := router.Run(address); err != nil {
|
||||
if err := engine.Run(address); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start server")
|
||||
}
|
||||
|
||||
@@ -203,7 +328,7 @@ func (app *BootstrapApp) heartbeat() {
|
||||
|
||||
var body heartbeat
|
||||
|
||||
body.UUID = app.context.uuid
|
||||
body.UUID = app.uuid
|
||||
body.Version = config.Version
|
||||
|
||||
bodyJson, err := json.Marshal(body)
|
||||
@@ -213,9 +338,7 @@ func (app *BootstrapApp) heartbeat() {
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(10) * time.Second, // The server should never take more than 10 seconds to respond
|
||||
}
|
||||
client := &http.Client{}
|
||||
|
||||
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"tinyauth/internal/controller"
|
||||
"tinyauth/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||
engine := gin.New()
|
||||
engine.Use(gin.Recovery())
|
||||
|
||||
if len(app.config.Server.TrustedProxies) > 0 {
|
||||
err := engine.SetTrustedProxies(strings.Split(app.config.Server.TrustedProxies, ","))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set trusted proxies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
|
||||
CookieDomain: app.context.cookieDomain,
|
||||
}, app.services.authService, app.services.oauthBrokerService)
|
||||
|
||||
err := contextMiddleware.Init()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize context middleware: %w", err)
|
||||
}
|
||||
|
||||
engine.Use(contextMiddleware.Middleware())
|
||||
|
||||
uiMiddleware := middleware.NewUIMiddleware()
|
||||
|
||||
err = uiMiddleware.Init()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize UI middleware: %w", err)
|
||||
}
|
||||
|
||||
engine.Use(uiMiddleware.Middleware())
|
||||
|
||||
zerologMiddleware := middleware.NewZerologMiddleware()
|
||||
|
||||
err = zerologMiddleware.Init()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize zerolog middleware: %w", err)
|
||||
}
|
||||
|
||||
engine.Use(zerologMiddleware.Middleware())
|
||||
|
||||
apiRouter := engine.Group("/api")
|
||||
|
||||
contextController := controller.NewContextController(controller.ContextControllerConfig{
|
||||
Providers: app.context.configuredProviders,
|
||||
Title: app.config.UI.Title,
|
||||
AppURL: app.config.AppURL,
|
||||
CookieDomain: app.context.cookieDomain,
|
||||
ForgotPasswordMessage: app.config.UI.ForgotPasswordMessage,
|
||||
BackgroundImage: app.config.UI.BackgroundImage,
|
||||
OAuthAutoRedirect: app.config.OAuth.AutoRedirect,
|
||||
DisableUIWarnings: app.config.DisableUIWarnings,
|
||||
}, apiRouter)
|
||||
|
||||
contextController.SetupRoutes()
|
||||
|
||||
oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{
|
||||
AppURL: app.config.AppURL,
|
||||
SecureCookie: app.config.Auth.SecureCookie,
|
||||
CSRFCookieName: app.context.csrfCookieName,
|
||||
RedirectCookieName: app.context.redirectCookieName,
|
||||
CookieDomain: app.context.cookieDomain,
|
||||
}, apiRouter, app.services.authService, app.services.oauthBrokerService)
|
||||
|
||||
oauthController.SetupRoutes()
|
||||
|
||||
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
|
||||
AppURL: app.config.AppURL,
|
||||
}, apiRouter, app.services.accessControlService, app.services.authService)
|
||||
|
||||
proxyController.SetupRoutes()
|
||||
|
||||
userController := controller.NewUserController(controller.UserControllerConfig{
|
||||
CookieDomain: app.context.cookieDomain,
|
||||
}, apiRouter, app.services.authService)
|
||||
|
||||
userController.SetupRoutes()
|
||||
|
||||
resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{
|
||||
ResourcesDir: app.config.ResourcesDir,
|
||||
ResourcesDisabled: app.config.DisableResources,
|
||||
}, &engine.RouterGroup)
|
||||
|
||||
resourcesController.SetupRoutes()
|
||||
|
||||
healthController := controller.NewHealthController(apiRouter)
|
||||
|
||||
healthController.SetupRoutes()
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"tinyauth/internal/service"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Services struct {
|
||||
accessControlService *service.AccessControlsService
|
||||
authService *service.AuthService
|
||||
databaseService *service.DatabaseService
|
||||
dockerService *service.DockerService
|
||||
ldapService *service.LdapService
|
||||
oauthBrokerService *service.OAuthBrokerService
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) initServices() (Services, error) {
|
||||
services := Services{}
|
||||
|
||||
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
|
||||
DatabasePath: app.config.DatabasePath,
|
||||
})
|
||||
|
||||
err := databaseService.Init()
|
||||
|
||||
if err != nil {
|
||||
return Services{}, err
|
||||
}
|
||||
|
||||
services.databaseService = databaseService
|
||||
|
||||
ldapService := service.NewLdapService(service.LdapServiceConfig{
|
||||
Address: app.config.Ldap.Address,
|
||||
BindDN: app.config.Ldap.BindDN,
|
||||
BindPassword: app.config.Ldap.BindPassword,
|
||||
BaseDN: app.config.Ldap.BaseDN,
|
||||
Insecure: app.config.Ldap.Insecure,
|
||||
SearchFilter: app.config.Ldap.SearchFilter,
|
||||
})
|
||||
|
||||
err = ldapService.Init()
|
||||
|
||||
if err == nil {
|
||||
services.ldapService = ldapService
|
||||
} else {
|
||||
log.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without it")
|
||||
}
|
||||
|
||||
dockerService := service.NewDockerService()
|
||||
|
||||
err = dockerService.Init()
|
||||
|
||||
if err != nil {
|
||||
return Services{}, err
|
||||
}
|
||||
|
||||
services.dockerService = dockerService
|
||||
|
||||
accessControlsService := service.NewAccessControlsService(dockerService)
|
||||
|
||||
err = accessControlsService.Init()
|
||||
|
||||
if err != nil {
|
||||
return Services{}, err
|
||||
}
|
||||
|
||||
services.accessControlService = accessControlsService
|
||||
|
||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||
Users: app.context.users,
|
||||
OauthWhitelist: app.config.OAuth.Whitelist,
|
||||
SessionExpiry: app.config.Auth.SessionExpiry,
|
||||
SecureCookie: app.config.Auth.SecureCookie,
|
||||
CookieDomain: app.context.cookieDomain,
|
||||
LoginTimeout: app.config.Auth.LoginTimeout,
|
||||
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
||||
SessionCookieName: app.context.sessionCookieName,
|
||||
}, dockerService, ldapService, databaseService.GetDatabase())
|
||||
|
||||
err = authService.Init()
|
||||
|
||||
if err != nil {
|
||||
return Services{}, err
|
||||
}
|
||||
|
||||
services.authService = authService
|
||||
|
||||
oauthBrokerService := service.NewOAuthBrokerService(app.context.oauthProviders)
|
||||
|
||||
err = oauthBrokerService.Init()
|
||||
|
||||
if err != nil {
|
||||
return Services{}, err
|
||||
}
|
||||
|
||||
services.oauthBrokerService = oauthBrokerService
|
||||
|
||||
return services, nil
|
||||
}
|
||||
@@ -15,61 +15,36 @@ var RedirectCookieName = "tinyauth-redirect"
|
||||
// Main app config
|
||||
|
||||
type Config struct {
|
||||
AppURL string `description:"The base URL where the app is hosted."`
|
||||
LogLevel string `description:"Log level (trace, debug, info, warn, error)."`
|
||||
ResourcesDir string `description:"The directory where resources are stored."`
|
||||
DatabasePath string `description:"The path to the database file."`
|
||||
DisableAnalytics bool `description:"Disable analytics."`
|
||||
DisableResources bool `description:"Disable resources server."`
|
||||
DisableUIWarnings bool `description:"Disable UI warnings."`
|
||||
Server ServerConfig
|
||||
Auth AuthConfig
|
||||
OAuth OAuthConfig
|
||||
UI UIConfig
|
||||
Ldap LdapConfig
|
||||
Port int `mapstructure:"port" validate:"required"`
|
||||
Address string `validate:"required,ip4_addr" mapstructure:"address"`
|
||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||
Users string `mapstructure:"users"`
|
||||
UsersFile string `mapstructure:"users-file"`
|
||||
SecureCookie bool `mapstructure:"secure-cookie"`
|
||||
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
||||
OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect"`
|
||||
SessionExpiry int `mapstructure:"session-expiry"`
|
||||
LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"`
|
||||
Title string `mapstructure:"app-title"`
|
||||
LoginTimeout int `mapstructure:"login-timeout"`
|
||||
LoginMaxRetries int `mapstructure:"login-max-retries"`
|
||||
ForgotPasswordMessage string `mapstructure:"forgot-password-message"`
|
||||
BackgroundImage string `mapstructure:"background-image" validate:"required"`
|
||||
LdapAddress string `mapstructure:"ldap-address"`
|
||||
LdapBindDN string `mapstructure:"ldap-bind-dn"`
|
||||
LdapBindPassword string `mapstructure:"ldap-bind-password"`
|
||||
LdapBaseDN string `mapstructure:"ldap-base-dn"`
|
||||
LdapInsecure bool `mapstructure:"ldap-insecure"`
|
||||
LdapSearchFilter string `mapstructure:"ldap-search-filter"`
|
||||
ResourcesDir string `mapstructure:"resources-dir"`
|
||||
DatabasePath string `mapstructure:"database-path" validate:"required"`
|
||||
TrustedProxies string `mapstructure:"trusted-proxies"`
|
||||
DisableAnalytics bool `mapstructure:"disable-analytics"`
|
||||
DisableResources bool `mapstructure:"disable-resources"`
|
||||
DisableUIWarnings bool `mapstructure:"disable-ui-warnings"`
|
||||
SocketPath string `mapstructure:"socket-path"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `description:"The port on which the server listens."`
|
||||
Address string `description:"The address on which the server listens."`
|
||||
SocketPath string `description:"The path to the Unix socket."`
|
||||
TrustedProxies string `description:"Comma-separated list of trusted proxy addresses."`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
Users string `description:"Comma-separated list of users (username:hashed_password)."`
|
||||
UsersFile string `description:"Path to the users file."`
|
||||
SecureCookie bool `description:"Enable secure cookies."`
|
||||
SessionExpiry int `description:"Session expiry time in seconds."`
|
||||
LoginTimeout int `description:"Login timeout in seconds."`
|
||||
LoginMaxRetries int `description:"Maximum login retries."`
|
||||
}
|
||||
|
||||
type OAuthConfig struct {
|
||||
Whitelist string `description:"Comma-separated list of allowed OAuth domains."`
|
||||
AutoRedirect string `description:"The OAuth provider to use for automatic redirection."`
|
||||
Providers map[string]OAuthServiceConfig
|
||||
}
|
||||
|
||||
type UIConfig struct {
|
||||
Title string `description:"The title of the UI."`
|
||||
ForgotPasswordMessage string `description:"Message displayed on the forgot password page."`
|
||||
BackgroundImage string `description:"Path to the background image."`
|
||||
}
|
||||
|
||||
type LdapConfig struct {
|
||||
Address string `description:"LDAP server address."`
|
||||
BindDN string `description:"Bind DN for LDAP authentication."`
|
||||
BindPassword string `description:"Bind password for LDAP authentication."`
|
||||
BaseDN string `description:"Base DN for LDAP searches."`
|
||||
Insecure bool `description:"Allow insecure LDAP connections."`
|
||||
SearchFilter string `description:"LDAP search filter."`
|
||||
}
|
||||
|
||||
// Config loader options
|
||||
|
||||
const DefaultNamePrefix = "TINYAUTH_"
|
||||
|
||||
// OAuth/OIDC config
|
||||
|
||||
type Claims struct {
|
||||
@@ -80,16 +55,16 @@ type Claims struct {
|
||||
}
|
||||
|
||||
type OAuthServiceConfig struct {
|
||||
ClientID string `description:"OAuth client ID."`
|
||||
ClientSecret string `description:"OAuth client secret."`
|
||||
ClientSecretFile string `description:"Path to the file containing the OAuth client secret."`
|
||||
Scopes []string `description:"OAuth scopes."`
|
||||
RedirectURL string `description:"OAuth redirect URL."`
|
||||
AuthURL string `description:"OAuth authorization URL."`
|
||||
TokenURL string `description:"OAuth token URL."`
|
||||
UserinfoURL string `description:"OAuth userinfo URL."`
|
||||
Insecure bool `description:"Allow insecure OAuth connections."`
|
||||
Name string `description:"Provider name in UI."`
|
||||
ClientID string `field:"client-id"`
|
||||
ClientSecret string
|
||||
ClientSecretFile string
|
||||
Scopes []string
|
||||
RedirectURL string `field:"redirect-url"`
|
||||
AuthURL string `field:"auth-url"`
|
||||
TokenURL string `field:"token-url"`
|
||||
UserinfoURL string `field:"user-info-url"`
|
||||
InsecureSkipVerify bool
|
||||
Name string
|
||||
}
|
||||
|
||||
var OverrideProviders = map[string]string{
|
||||
|
||||
@@ -2,9 +2,6 @@ package service
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"tinyauth/internal/assets"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
@@ -30,17 +27,7 @@ func NewDatabaseService(config DatabaseServiceConfig) *DatabaseService {
|
||||
}
|
||||
|
||||
func (ds *DatabaseService) Init() error {
|
||||
dbPath := ds.config.DatabasePath
|
||||
if dbPath == "" {
|
||||
dbPath = "/data/tinyauth.db"
|
||||
}
|
||||
|
||||
dir := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create database directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
gormDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
gormDB, err := gorm.Open(sqlite.Open(ds.config.DatabasePath), &gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -82,7 +82,7 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
|
||||
return config.App{}, err
|
||||
}
|
||||
|
||||
labels, err := decoders.DecodeLabels[config.Apps](inspect.Config.Labels, "apps")
|
||||
labels, err := decoders.DecodeLabels(inspect.Config.Labels)
|
||||
if err != nil {
|
||||
return config.App{}, err
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthServi
|
||||
TokenURL: config.TokenURL,
|
||||
},
|
||||
},
|
||||
insecureSkipVerify: config.Insecure,
|
||||
insecureSkipVerify: config.InsecureSkipVerify,
|
||||
userinfoUrl: config.UserinfoURL,
|
||||
name: config.Name,
|
||||
}
|
||||
|
||||
@@ -6,8 +6,12 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils/decoders"
|
||||
|
||||
"maps"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/weppos/publicsuffix-go/publicsuffix"
|
||||
)
|
||||
|
||||
@@ -100,3 +104,119 @@ func IsRedirectSafe(redirectURL string, domain string) bool {
|
||||
|
||||
return hostname == domain
|
||||
}
|
||||
|
||||
func GetLogLevel(level string) zerolog.Level {
|
||||
switch strings.ToLower(level) {
|
||||
case "trace":
|
||||
return zerolog.TraceLevel
|
||||
case "debug":
|
||||
return zerolog.DebugLevel
|
||||
case "info":
|
||||
return zerolog.InfoLevel
|
||||
case "warn":
|
||||
return zerolog.WarnLevel
|
||||
case "error":
|
||||
return zerolog.ErrorLevel
|
||||
case "fatal":
|
||||
return zerolog.FatalLevel
|
||||
case "panic":
|
||||
return zerolog.PanicLevel
|
||||
default:
|
||||
return zerolog.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[string]config.OAuthServiceConfig, error) {
|
||||
providers := make(map[string]config.OAuthServiceConfig)
|
||||
|
||||
// Get from environment variables
|
||||
envMap := make(map[string]string)
|
||||
|
||||
for _, e := range env {
|
||||
pair := strings.SplitN(e, "=", 2)
|
||||
if len(pair) == 2 {
|
||||
envMap[pair[0]] = pair[1]
|
||||
}
|
||||
}
|
||||
|
||||
envProviders, err := decoders.DecodeEnv[config.Providers, config.OAuthServiceConfig](envMap, "providers")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maps.Copy(providers, envProviders.Providers)
|
||||
|
||||
// Get from flags
|
||||
flagsMap := make(map[string]string)
|
||||
|
||||
for _, arg := range args[1:] {
|
||||
if strings.HasPrefix(arg, "--") {
|
||||
pair := strings.SplitN(arg[2:], "=", 2)
|
||||
if len(pair) == 2 {
|
||||
flagsMap[pair[0]] = pair[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flagProviders, err := decoders.DecodeFlags[config.Providers, config.OAuthServiceConfig](flagsMap, "providers")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maps.Copy(providers, flagProviders.Providers)
|
||||
|
||||
// For every provider get correct secret from file if set
|
||||
for name, provider := range providers {
|
||||
secret := GetSecret(provider.ClientSecret, provider.ClientSecretFile)
|
||||
provider.ClientSecret = secret
|
||||
provider.ClientSecretFile = ""
|
||||
providers[name] = provider
|
||||
}
|
||||
|
||||
// If we have google/github providers and no redirect URL then set a default
|
||||
for id := range config.OverrideProviders {
|
||||
if provider, exists := providers[id]; exists {
|
||||
if provider.RedirectURL == "" {
|
||||
provider.RedirectURL = appUrl + "/api/oauth/callback/" + id
|
||||
providers[id] = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set names
|
||||
for id, provider := range providers {
|
||||
if provider.Name == "" {
|
||||
if name, ok := config.OverrideProviders[id]; ok {
|
||||
provider.Name = name
|
||||
} else {
|
||||
provider.Name = Capitalize(id)
|
||||
}
|
||||
}
|
||||
providers[id] = provider
|
||||
}
|
||||
|
||||
// Return combined providers
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func ShoudLogJSON(environ []string, args []string) bool {
|
||||
for _, e := range environ {
|
||||
pair := strings.SplitN(e, "=", 2)
|
||||
if len(pair) == 2 && pair[0] == "LOG_JSON" && strings.ToLower(pair[1]) == "true" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range args[1:] {
|
||||
if strings.HasPrefix(arg, "--log-json=") {
|
||||
value := strings.SplitN(arg, "=", 2)[1]
|
||||
if strings.ToLower(value) == "true" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils"
|
||||
@@ -205,3 +206,93 @@ func TestIsRedirectSafe(t *testing.T) {
|
||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||
assert.Equal(t, false, result)
|
||||
}
|
||||
|
||||
func TestGetOAuthProvidersConfig(t *testing.T) {
|
||||
env := []string{"PROVIDERS_CLIENT1_CLIENT_ID=client1-id", "PROVIDERS_CLIENT1_CLIENT_SECRET=client1-secret"}
|
||||
args := []string{"/tinyauth/tinyauth", "--providers-client2-client-id=client2-id", "--providers-client2-client-secret=client2-secret"}
|
||||
|
||||
expected := map[string]config.OAuthServiceConfig{
|
||||
"client1": {
|
||||
ClientID: "client1-id",
|
||||
ClientSecret: "client1-secret",
|
||||
Name: "Client1",
|
||||
},
|
||||
"client2": {
|
||||
ClientID: "client2-id",
|
||||
ClientSecret: "client2-secret",
|
||||
Name: "Client2",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := utils.GetOAuthProvidersConfig(env, args, "")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expected, result)
|
||||
|
||||
// Case with no providers
|
||||
env = []string{}
|
||||
args = []string{"/tinyauth/tinyauth"}
|
||||
expected = map[string]config.OAuthServiceConfig{}
|
||||
|
||||
result, err = utils.GetOAuthProvidersConfig(env, args, "")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expected, result)
|
||||
|
||||
// Case with secret from file
|
||||
file, err := os.Create("/tmp/tinyauth_test_file")
|
||||
assert.NilError(t, err)
|
||||
|
||||
_, err = file.WriteString("file content\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = file.Close()
|
||||
assert.NilError(t, err)
|
||||
defer os.Remove("/tmp/tinyauth_test_file")
|
||||
|
||||
env = []string{"PROVIDERS_CLIENT1_CLIENT_ID=client1-id", "PROVIDERS_CLIENT1_CLIENT_SECRET_FILE=/tmp/tinyauth_test_file"}
|
||||
args = []string{"/tinyauth/tinyauth"}
|
||||
expected = map[string]config.OAuthServiceConfig{
|
||||
"client1": {
|
||||
ClientID: "client1-id",
|
||||
ClientSecret: "file content",
|
||||
Name: "Client1",
|
||||
},
|
||||
}
|
||||
|
||||
result, err = utils.GetOAuthProvidersConfig(env, args, "")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expected, result)
|
||||
|
||||
// Case with google provider and no redirect URL
|
||||
env = []string{"PROVIDERS_GOOGLE_CLIENT_ID=google-id", "PROVIDERS_GOOGLE_CLIENT_SECRET=google-secret"}
|
||||
args = []string{"/tinyauth/tinyauth"}
|
||||
expected = map[string]config.OAuthServiceConfig{
|
||||
"google": {
|
||||
ClientID: "google-id",
|
||||
ClientSecret: "google-secret",
|
||||
RedirectURL: "http://app.url/api/oauth/callback/google",
|
||||
Name: "Google",
|
||||
},
|
||||
}
|
||||
|
||||
result, err = utils.GetOAuthProvidersConfig(env, args, "http://app.url")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expected, result)
|
||||
}
|
||||
|
||||
func TestShoudLogJSON(t *testing.T) {
|
||||
// Test with no env or args
|
||||
result := utils.ShoudLogJSON([]string{"FOO=bar"}, []string{"tinyauth", "--foo-bar=baz"})
|
||||
assert.Equal(t, false, result)
|
||||
|
||||
// Test with env variable set
|
||||
result = utils.ShoudLogJSON([]string{"LOG_JSON=true"}, []string{"tinyauth", "--foo-bar=baz"})
|
||||
assert.Equal(t, true, result)
|
||||
|
||||
// Test with flag set
|
||||
result = utils.ShoudLogJSON([]string{"FOO=bar"}, []string{"tinyauth", "--log-json=true"})
|
||||
assert.Equal(t, true, result)
|
||||
|
||||
// Test with both env and flag set to false
|
||||
result = utils.ShoudLogJSON([]string{"LOG_JSON=false"}, []string{"tinyauth", "--log-json=false"})
|
||||
assert.Equal(t, false, result)
|
||||
}
|
||||
|
||||
80
internal/utils/decoders/decoders.go
Normal file
80
internal/utils/decoders/decoders.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package decoders
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/stoewer/go-strcase"
|
||||
)
|
||||
|
||||
func normalizeKeys[T any](input map[string]string, root string, sep string) map[string]string {
|
||||
knownKeys := getKnownKeys[T]()
|
||||
normalized := make(map[string]string)
|
||||
|
||||
for k, v := range input {
|
||||
parts := []string{"tinyauth"}
|
||||
|
||||
key := strings.ToLower(k)
|
||||
key = strings.ReplaceAll(key, sep, "-")
|
||||
|
||||
if !strings.HasPrefix(key, root+"-") {
|
||||
continue
|
||||
}
|
||||
|
||||
suffix := ""
|
||||
|
||||
for _, known := range knownKeys {
|
||||
if strings.HasSuffix(key, known) {
|
||||
suffix = known
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if suffix == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts = append(parts, root)
|
||||
|
||||
id := strings.TrimPrefix(key, root+"-")
|
||||
id = strings.TrimSuffix(id, "-"+suffix)
|
||||
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts = append(parts, id)
|
||||
parts = append(parts, suffix)
|
||||
|
||||
final := ""
|
||||
|
||||
for i, part := range parts {
|
||||
if i > 0 {
|
||||
final += "."
|
||||
}
|
||||
final += strcase.LowerCamelCase(part)
|
||||
}
|
||||
|
||||
normalized[final] = v
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
func getKnownKeys[T any]() []string {
|
||||
var keys []string
|
||||
var t T
|
||||
|
||||
v := reflect.ValueOf(t)
|
||||
typeOfT := v.Type()
|
||||
|
||||
for field := range typeOfT.NumField() {
|
||||
if typeOfT.Field(field).Tag.Get("field") != "" {
|
||||
keys = append(keys, typeOfT.Field(field).Tag.Get("field"))
|
||||
continue
|
||||
}
|
||||
keys = append(keys, strcase.KebabCase(typeOfT.Field(field).Name))
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
19
internal/utils/decoders/env_decoder.go
Normal file
19
internal/utils/decoders/env_decoder.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package decoders
|
||||
|
||||
import (
|
||||
"github.com/traefik/paerser/parser"
|
||||
)
|
||||
|
||||
func DecodeEnv[T any, C any](env map[string]string, subName string) (T, error) {
|
||||
var result T
|
||||
|
||||
normalized := normalizeKeys[C](env, subName, "_")
|
||||
|
||||
err := parser.Decode(normalized, &result, "tinyauth", "tinyauth."+subName)
|
||||
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
37
internal/utils/decoders/env_decoder_test.go
Normal file
37
internal/utils/decoders/env_decoder_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package decoders_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils/decoders"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestDecodeEnv(t *testing.T) {
|
||||
// Setup
|
||||
env := map[string]string{
|
||||
"PROVIDERS_GOOGLE_CLIENT_ID": "google-client-id",
|
||||
"PROVIDERS_GOOGLE_CLIENT_SECRET": "google-client-secret",
|
||||
"PROVIDERS_MY_GITHUB_CLIENT_ID": "github-client-id",
|
||||
"PROVIDERS_MY_GITHUB_CLIENT_SECRET": "github-client-secret",
|
||||
}
|
||||
|
||||
expected := config.Providers{
|
||||
Providers: map[string]config.OAuthServiceConfig{
|
||||
"google": {
|
||||
ClientID: "google-client-id",
|
||||
ClientSecret: "google-client-secret",
|
||||
},
|
||||
"myGithub": {
|
||||
ClientID: "github-client-id",
|
||||
ClientSecret: "github-client-secret",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Execute
|
||||
result, err := decoders.DecodeEnv[config.Providers, config.OAuthServiceConfig](env, "providers")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, result, expected)
|
||||
}
|
||||
30
internal/utils/decoders/flags_decoder.go
Normal file
30
internal/utils/decoders/flags_decoder.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package decoders
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/traefik/paerser/parser"
|
||||
)
|
||||
|
||||
func DecodeFlags[T any, C any](flags map[string]string, subName string) (T, error) {
|
||||
var result T
|
||||
|
||||
filtered := filterFlags(flags)
|
||||
normalized := normalizeKeys[C](filtered, subName, "_")
|
||||
|
||||
err := parser.Decode(normalized, &result, "tinyauth", "tinyauth."+subName)
|
||||
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func filterFlags(flags map[string]string) map[string]string {
|
||||
filtered := make(map[string]string)
|
||||
for k, v := range flags {
|
||||
filtered[strings.TrimPrefix(k, "--")] = v
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
37
internal/utils/decoders/flags_decoder_test.go
Normal file
37
internal/utils/decoders/flags_decoder_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package decoders_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils/decoders"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestDecodeFlags(t *testing.T) {
|
||||
// Setup
|
||||
flags := map[string]string{
|
||||
"--providers-google-client-id": "google-client-id",
|
||||
"--providers-google-client-secret": "google-client-secret",
|
||||
"--providers-my-github-client-id": "github-client-id",
|
||||
"--providers-my-github-client-secret": "github-client-secret",
|
||||
}
|
||||
|
||||
expected := config.Providers{
|
||||
Providers: map[string]config.OAuthServiceConfig{
|
||||
"google": {
|
||||
ClientID: "google-client-id",
|
||||
ClientSecret: "google-client-secret",
|
||||
},
|
||||
"myGithub": {
|
||||
ClientID: "github-client-id",
|
||||
ClientSecret: "github-client-secret",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Execute
|
||||
result, err := decoders.DecodeFlags[config.Providers, config.OAuthServiceConfig](flags, "providers")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, result, expected)
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
package decoders
|
||||
|
||||
import (
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"github.com/traefik/paerser/parser"
|
||||
)
|
||||
|
||||
func DecodeLabels[T any](labels map[string]string, root string) (T, error) {
|
||||
var labelsDecoded T
|
||||
func DecodeLabels(labels map[string]string) (config.Apps, error) {
|
||||
var appLabels config.Apps
|
||||
|
||||
err := parser.Decode(labels, &labelsDecoded, "tinyauth", "tinyauth."+root)
|
||||
err := parser.Decode(labels, &appLabels, "tinyauth", "tinyauth.apps")
|
||||
|
||||
if err != nil {
|
||||
return labelsDecoded, err
|
||||
return config.Apps{}, err
|
||||
}
|
||||
|
||||
return labelsDecoded, nil
|
||||
return appLabels, nil
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestDecodeLabels(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test
|
||||
result, err := decoders.DecodeLabels[config.Apps](test, "apps")
|
||||
result, err := decoders.DecodeLabels(test)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expected, result)
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"github.com/traefik/paerser/cli"
|
||||
"github.com/traefik/paerser/env"
|
||||
)
|
||||
|
||||
type EnvLoader struct{}
|
||||
|
||||
func (e *EnvLoader) Load(_ []string, cmd *cli.Command) (bool, error) {
|
||||
vars := env.FindPrefixedEnvVars(os.Environ(), config.DefaultNamePrefix, cmd.Configuration)
|
||||
if len(vars) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := env.Decode(vars, config.DefaultNamePrefix, cmd.Configuration); err != nil {
|
||||
return false, fmt.Errorf("failed to decode configuration from environment variables: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/traefik/paerser/cli"
|
||||
"github.com/traefik/paerser/flag"
|
||||
)
|
||||
|
||||
type FlagLoader struct{}
|
||||
|
||||
func (*FlagLoader) Load(args []string, cmd *cli.Command) (bool, error) {
|
||||
if len(args) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := flag.Decode(args, cmd.Configuration); err != nil {
|
||||
return false, fmt.Errorf("failed to decode configuration from flags: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
19
main.go
Normal file
19
main.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
"tinyauth/cmd"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Logger = log.Logger.With().Caller().Logger()
|
||||
if !utils.ShoudLogJSON(os.Environ(), os.Args) {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
|
||||
}
|
||||
cmd.Run()
|
||||
}
|
||||
Reference in New Issue
Block a user