mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-01-10 17:32:31 +00:00
Compare commits
11 Commits
shreknel-f
...
ed28e7a218
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed28e7a218 | ||
|
|
7db81121e1 | ||
|
|
195b70b4d7 | ||
|
|
c4529be557 | ||
|
|
0374370b0c | ||
|
|
7857dba57a | ||
|
|
3e12721844 | ||
|
|
9c7a4af295 | ||
|
|
dba5580a7c | ||
|
|
e4e99f4805 | ||
|
|
3555569a97 |
@@ -1,3 +0,0 @@
|
||||
issue_enrichment:
|
||||
auto_enrich:
|
||||
enabled: false
|
||||
74
.env.example
74
.env.example
@@ -1,86 +1,86 @@
|
||||
# Base Configuration
|
||||
|
||||
# The base URL where Tinyauth is accessible
|
||||
TINYAUTH_APPURL="https://auth.example.com"
|
||||
TINYAUTH_APPURL=https://auth.example.com
|
||||
# Log level: trace, debug, info, warn, error
|
||||
TINYAUTH_LOGLEVEL="info"
|
||||
TINYAUTH_LOGLEVEL=info
|
||||
# Directory for static resources
|
||||
TINYAUTH_RESOURCESDIR="/data/resources"
|
||||
TINYAUTH_RESOURCESDIR=/data/resources
|
||||
# Path to SQLite database file
|
||||
TINYAUTH_DATABASEPATH="/data/tinyauth.db"
|
||||
TINYAUTH_DATABASEPATH=/data/tinyauth.db
|
||||
# Disable version heartbeat
|
||||
TINYAUTH_DISABLEANALYTICS="false"
|
||||
TINYAUTH_DISABLEANALYTICS=false
|
||||
# Disable static resource serving
|
||||
TINYAUTH_DISABLERESOURCES="false"
|
||||
TINYAUTH_DISABLERESOURCES=false
|
||||
# Disable UI warning messages
|
||||
TINYAUTH_DISABLEUIWARNINGS="false"
|
||||
TINYAUTH_DISABLEUIWARNINGS=false
|
||||
# Enable JSON formatted logs
|
||||
TINYAUTH_LOGJSON="false"
|
||||
TINYAUTH_LOGJSON=false
|
||||
|
||||
# Server Configuration
|
||||
|
||||
# Port to listen on
|
||||
TINYAUTH_SERVER_PORT="3000"
|
||||
TINYAUTH_SERVER_PORT=3000
|
||||
# Interface to bind to (0.0.0.0 for all interfaces)
|
||||
TINYAUTH_SERVER_ADDRESS="0.0.0.0"
|
||||
TINYAUTH_SERVER_ADDRESS=0.0.0.0
|
||||
# Unix socket path (optional, overrides port/address if set)
|
||||
TINYAUTH_SERVER_SOCKETPATH=""
|
||||
TINYAUTH_SERVER_SOCKETPATH=
|
||||
# Comma-separated list of trusted proxy IPs/CIDRs
|
||||
TINYAUTH_SERVER_TRUSTEDPROXIES=""
|
||||
TINYAUTH_SERVER_TRUSTEDPROXIES=
|
||||
|
||||
# Authentication Configuration
|
||||
|
||||
# Format: username:bcrypt_hash (use bcrypt to generate hash)
|
||||
TINYAUTH_AUTH_USERS="admin:$2a$10$example_bcrypt_hash_here"
|
||||
TINYAUTH_AUTH_USERS=admin:$2a$10$example_bcrypt_hash_here
|
||||
# Path to external users file (optional)
|
||||
TINYAUTH_USERSFILE=""
|
||||
TINYAUTH_USERSFILE=
|
||||
# Enable secure cookies (requires HTTPS)
|
||||
TINYAUTH_SECURECOOKIE="true"
|
||||
TINYAUTH_SECURECOOKIE=true
|
||||
# Session expiry in seconds (7200 = 2 hours)
|
||||
TINYAUTH_SESSIONEXPIRY="7200"
|
||||
TINYAUTH_SESSIONEXPIRY=7200
|
||||
# Login timeout in seconds (300 = 5 minutes)
|
||||
TINYAUTH_LOGINTIMEOUT="300"
|
||||
TINYAUTH_LOGINTIMEOUT=300
|
||||
# Maximum login retries before lockout
|
||||
TINYAUTH_LOGINMAXRETRIES="5"
|
||||
TINYAUTH_LOGINMAXRETRIES=5
|
||||
|
||||
# OAuth Configuration
|
||||
|
||||
# Regex pattern for allowed email addresses (e.g., /@example\.com$/)
|
||||
TINYAUTH_OAUTH_WHITELIST=""
|
||||
TINYAUTH_OAUTH_WHITELIST=
|
||||
# Provider ID to auto-redirect to (skips login page)
|
||||
TINYAUTH_OAUTH_AUTOREDIRECT=""
|
||||
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"
|
||||
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"
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_INSECURE=false
|
||||
|
||||
# UI Customization
|
||||
|
||||
# Custom title for login page
|
||||
TINYAUTH_UI_TITLE="Tinyauth"
|
||||
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=""
|
||||
TINYAUTH_UI_BACKGROUNDIMAGE=
|
||||
|
||||
# LDAP Configuration
|
||||
|
||||
# LDAP server address
|
||||
TINYAUTH_LDAP_ADDRESS="ldap://ldap.example.com:389"
|
||||
TINYAUTH_LDAP_ADDRESS=ldap://ldap.example.com:389
|
||||
# DN for binding to LDAP server
|
||||
TINYAUTH_LDAP_BINDDN="cn=readonly,dc=example,dc=com"
|
||||
TINYAUTH_LDAP_BINDDN=cn=readonly,dc=example,dc=com
|
||||
# Password for bind DN
|
||||
TINYAUTH_LDAP_BINDPASSWORD="your_bind_password"
|
||||
TINYAUTH_LDAP_BINDPASSWORD=your_bind_password
|
||||
# Base DN for user searches
|
||||
TINYAUTH_LDAP_BASEDN="dc=example,dc=com"
|
||||
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))"
|
||||
TINYAUTH_LDAP_SEARCHFILTER=(&(uid=%s)(memberOf=cn=users,ou=groups,dc=example,dc=com))
|
||||
# Allow insecure LDAP connections
|
||||
TINYAUTH_LDAP_INSECURE="false"
|
||||
TINYAUTH_LDAP_INSECURE=false
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -33,7 +33,4 @@
|
||||
|
||||
# binary out
|
||||
/tinyauth.db
|
||||
/resources
|
||||
|
||||
# debug files
|
||||
__debug_*
|
||||
/resources
|
||||
@@ -1 +0,0 @@
|
||||
# Trigger automated review
|
||||
@@ -1,5 +1,5 @@
|
||||
# Site builder
|
||||
FROM oven/bun:1.3.5-alpine AS frontend-builder
|
||||
FROM oven/bun:1.3.4-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ COPY ./air.toml ./
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV TINYAUTH_DATABASEPATH=/data/tinyauth.db
|
||||
ENV DATABASEPATH=/data/tinyauth.db
|
||||
|
||||
ENV TINYAUTH_RESOURCESDIR=/data/resources
|
||||
ENV RESOURCESDIR=/data/resources
|
||||
|
||||
ENTRYPOINT ["air", "-c", "air.toml"]
|
||||
@@ -1,5 +1,5 @@
|
||||
# Site builder
|
||||
FROM oven/bun:1.3.5-alpine AS frontend-builder
|
||||
FROM oven/bun:1.3.4-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
@@ -55,9 +55,9 @@ EXPOSE 3000
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENV TINYAUTH_DATABASEPATH=/data/tinyauth.db
|
||||
ENV DATABASEPATH=/data/tinyauth.db
|
||||
|
||||
ENV TINYAUTH_RESOURCESDIR=/data/resources
|
||||
ENV RESOURCESDIR=/data/resources
|
||||
|
||||
ENV GIN_MODE=release
|
||||
|
||||
|
||||
@@ -33,8 +33,6 @@ If you are still not sure if Tinyauth suits your needs you can try out the [demo
|
||||
|
||||
You can find documentation and guides on all of the available configuration of Tinyauth in the [website](https://tinyauth.app).
|
||||
|
||||
If you wish to contribute to the documentation head over to the [repository](https://github.com/steveiliop56/tinyauth-docs).
|
||||
|
||||
## Discord
|
||||
|
||||
Tinyauth has a [discord](https://discord.gg/eHzVaCzRRd) server. Feel free to hop in to chat about self-hosting, homelabs and of course Tinyauth. See you there!
|
||||
|
||||
@@ -6,8 +6,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/mdp/qrterminal/v3"
|
||||
|
||||
@@ -29,14 +29,14 @@ func healthcheckCmd() *cli.Command {
|
||||
Run: func(args []string) error {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
||||
|
||||
appUrl := os.Getenv("TINYAUTH_APPURL")
|
||||
appUrl := os.Getenv("APPURL")
|
||||
|
||||
if len(args) > 0 {
|
||||
appUrl = args[0]
|
||||
}
|
||||
|
||||
if appUrl == "" {
|
||||
return errors.New("TINYAUTH_APPURL is not set and no argument was provided")
|
||||
return errors.New("APPURL is not set and no argument was provided")
|
||||
}
|
||||
|
||||
log.Info().Str("app_url", appUrl).Msg("Performing health check")
|
||||
|
||||
@@ -5,10 +5,9 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/loaders"
|
||||
"tinyauth/internal/bootstrap"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils/loaders"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -34,10 +33,6 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
||||
ForgotPasswordMessage: "You can change your password by changing the configuration.",
|
||||
BackgroundImage: "/background.jpg",
|
||||
},
|
||||
Ldap: config.LdapConfig{
|
||||
Insecure: false,
|
||||
SearchFilter: "(uid=%s)",
|
||||
},
|
||||
Experimental: config.ExperimentalConfig{
|
||||
ConfigFile: "",
|
||||
},
|
||||
|
||||
@@ -5,8 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
@@ -2,8 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"github.com/traefik/paerser/cli"
|
||||
)
|
||||
|
||||
@@ -63,42 +63,6 @@ oauth:
|
||||
# Allow insecure connections (self-signed certificates)
|
||||
insecure: false
|
||||
|
||||
# OIDC Provider Configuration
|
||||
oidc:
|
||||
# Enable OIDC provider functionality
|
||||
enabled: false
|
||||
# OIDC issuer URL (defaults to appUrl if not set)
|
||||
issuer: ""
|
||||
# Access token expiry in seconds (3600 = 1 hour)
|
||||
accessTokenExpiry: 3600
|
||||
# ID token expiry in seconds (3600 = 1 hour)
|
||||
idTokenExpiry: 3600
|
||||
# OIDC Client Configuration
|
||||
clients:
|
||||
# Client ID (used as the key)
|
||||
myapp:
|
||||
# Client secret (or use clientSecretFile)
|
||||
clientSecret: "your_client_secret_here"
|
||||
# Path to file containing client secret (optional, alternative to clientSecret)
|
||||
clientSecretFile: ""
|
||||
# Client name for display purposes
|
||||
clientName: "My Application"
|
||||
# Allowed redirect URIs
|
||||
redirectUris:
|
||||
- "https://myapp.example.com/callback"
|
||||
- "http://localhost:3000/callback"
|
||||
# Allowed grant types (defaults to ["authorization_code"] if not specified)
|
||||
grantTypes:
|
||||
- "authorization_code"
|
||||
# Allowed response types (defaults to ["code"] if not specified)
|
||||
responseTypes:
|
||||
- "code"
|
||||
# Allowed scopes (defaults to ["openid", "profile", "email"] if not specified)
|
||||
scopes:
|
||||
- "openid"
|
||||
- "profile"
|
||||
- "email"
|
||||
|
||||
# UI Customization
|
||||
ui:
|
||||
# Custom title for login page
|
||||
|
||||
@@ -11,44 +11,44 @@
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^25.7.3",
|
||||
"i18next": "^25.7.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "^0.556.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-i18next": "^16.4.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.11.0",
|
||||
"react-router": "^7.10.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"zod": "^4.2.1",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"zod": "^4.1.13",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/node": "^24.10.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"prettier": "3.7.4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.50.0",
|
||||
"vite": "^7.3.0",
|
||||
"typescript-eslint": "^8.49.0",
|
||||
"vite": "^7.2.7",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -93,57 +93,55 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
|
||||
|
||||
@@ -157,7 +155,7 @@
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="],
|
||||
"@eslint/js": ["@eslint/js@9.39.1", "", {}, "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
@@ -307,35 +305,35 @@
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.17", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.17", "@tailwindcss/oxide-darwin-arm64": "4.1.17", "@tailwindcss/oxide-darwin-x64": "4.1.17", "@tailwindcss/oxide-freebsd-x64": "4.1.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", "@tailwindcss/oxide-linux-x64-musl": "4.1.17", "@tailwindcss/oxide-wasm32-wasi": "4.1.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.17", "", { "os": "android", "cpu": "arm64" }, "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17", "", { "os": "linux", "cpu": "arm" }, "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.17", "", { "dependencies": { "@emnapi/core": "^1.6.0", "@emnapi/runtime": "^1.6.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.17", "", { "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -365,7 +363,7 @@
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||
"@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||
|
||||
@@ -373,25 +371,25 @@
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/type-utils": "8.50.0", "@typescript-eslint/utils": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg=="],
|
||||
"@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/parser": ["@typescript-eslint/parser@8.50.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q=="],
|
||||
"@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/project-service": ["@typescript-eslint/project-service@8.50.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.50.0", "@typescript-eslint/types": "^8.50.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ=="],
|
||||
"@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/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.50.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w=="],
|
||||
"@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/type-utils": ["@typescript-eslint/type-utils@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.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-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw=="],
|
||||
"@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/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.50.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.50.0", "@typescript-eslint/tsconfig-utils": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.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-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ=="],
|
||||
"@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/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.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q=="],
|
||||
"@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=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
@@ -493,17 +491,17 @@
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||
"esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
|
||||
"eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
||||
|
||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="],
|
||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.24", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
@@ -589,7 +587,7 @@
|
||||
|
||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||
|
||||
"i18next": ["i18next@25.7.3", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA=="],
|
||||
"i18next": ["i18next@25.7.2", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-58b4kmLpLv1buWUEwegMDUqZVR5J+rT+WTRFaBGL7lxDuJQQ0NrJFrq+eT2N94aYVR1k1Sr13QITNOL88tZCuw=="],
|
||||
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
||||
|
||||
@@ -675,7 +673,7 @@
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
|
||||
"lucide-react": ["lucide-react@0.556.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
@@ -791,13 +789,13 @@
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||
"react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||
"react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.68.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q=="],
|
||||
|
||||
"react-i18next": ["react-i18next@16.5.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-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw=="],
|
||||
"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-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=="],
|
||||
|
||||
@@ -807,7 +805,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.11.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ=="],
|
||||
"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-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=="],
|
||||
|
||||
@@ -851,7 +849,7 @@
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
|
||||
|
||||
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
|
||||
|
||||
@@ -873,7 +871,7 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.50.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.0", "@typescript-eslint/parser": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A=="],
|
||||
"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=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -903,7 +901,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.3.0", "", { "dependencies": { "esbuild": "^0.27.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-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
|
||||
"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=="],
|
||||
|
||||
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
|
||||
|
||||
@@ -915,7 +913,7 @@
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
||||
"zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
|
||||
|
||||
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
||||
|
||||
@@ -969,13 +967,13 @@
|
||||
|
||||
"@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
@@ -995,25 +993,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.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="],
|
||||
"@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/utils": ["@typescript-eslint/utils@8.50.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg=="],
|
||||
"@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/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="],
|
||||
"@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/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
||||
"@typescript-eslint/parser/@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.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
|
||||
"@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.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
||||
"@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/utils": ["@typescript-eslint/utils@8.50.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg=="],
|
||||
"@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/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
||||
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
@@ -1021,7 +1019,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.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
||||
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1039,7 +1037,7 @@
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.50.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg=="],
|
||||
"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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -1057,11 +1055,15 @@
|
||||
|
||||
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
||||
"@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/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="],
|
||||
"@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/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
|
||||
"@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/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
@@ -1081,9 +1083,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.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="],
|
||||
"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/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="],
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
|
||||
@@ -17,43 +17,43 @@
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^25.7.3",
|
||||
"i18next": "^25.7.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "^0.556.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-i18next": "^16.4.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.11.0",
|
||||
"react-router": "^7.10.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"zod": "^4.2.1"
|
||||
"tailwindcss": "^4.1.17",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/node": "^24.10.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"prettier": "3.7.4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.50.0",
|
||||
"vite": "^7.3.0"
|
||||
"typescript-eslint": "^8.49.0",
|
||||
"vite": "^7.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
107
go.mod
107
go.mod
@@ -1,4 +1,4 @@
|
||||
module github.com/steveiliop56/tinyauth
|
||||
module tinyauth
|
||||
|
||||
go 1.24.0
|
||||
|
||||
@@ -6,33 +6,69 @@ toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/charmbracelet/huh v0.8.0
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/golang-jwt/jwt/v5 v5.3.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/pquerna/otp v1.5.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
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/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
gorm.io/gorm v1.31.1
|
||||
gotest.tools/v3 v3.5.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.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
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
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/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/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
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.2 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
@@ -42,17 +78,14 @@ require (
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.6 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/huh v0.8.0
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
@@ -60,21 +93,13 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gin-contrib/sse v1.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-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // 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.11 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // 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
|
||||
@@ -82,49 +107,31 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // 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/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.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
|
||||
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/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.2 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
)
|
||||
|
||||
87
go.sum
87
go.sum
@@ -2,15 +2,15 @@ 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.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
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.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/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=
|
||||
@@ -32,8 +32,6 @@ 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=
|
||||
@@ -127,8 +125,6 @@ github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@@ -142,15 +138,16 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC
|
||||
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.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
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/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 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
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/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=
|
||||
@@ -167,8 +164,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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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/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=
|
||||
@@ -215,8 +212,9 @@ 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=
|
||||
@@ -229,8 +227,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.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
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/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=
|
||||
@@ -242,8 +240,8 @@ 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.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
|
||||
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
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/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=
|
||||
@@ -288,22 +286,22 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
|
||||
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.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.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=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
@@ -353,8 +351,8 @@ 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.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
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=
|
||||
@@ -363,19 +361,18 @@ 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/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-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=
|
||||
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=
|
||||
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 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
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=
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "sessions" DROP COLUMN "oauth_sub";
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "sessions" ADD COLUMN "oauth_sub" TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS "oidc_clients";
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "oidc_clients" (
|
||||
"client_id" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||
"client_secret" TEXT NOT NULL,
|
||||
"client_name" TEXT NOT NULL,
|
||||
"redirect_uris" TEXT NOT NULL,
|
||||
"grant_types" TEXT NOT NULL,
|
||||
"response_types" TEXT NOT NULL,
|
||||
"scopes" TEXT NOT NULL,
|
||||
"created_at" INTEGER NOT NULL,
|
||||
"updated_at" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS "oidc_keys";
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "oidc_keys" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"private_key" TEXT NOT NULL,
|
||||
"created_at" INTEGER NOT NULL,
|
||||
"updated_at" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
DROP INDEX IF EXISTS "idx_oidc_auth_codes_expires_at";
|
||||
DROP TABLE IF EXISTS "oidc_authorization_codes";
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "oidc_authorization_codes" (
|
||||
"code" TEXT NOT NULL PRIMARY KEY,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"redirect_uri" TEXT NOT NULL,
|
||||
"used" BOOLEAN NOT NULL DEFAULT 0,
|
||||
"expires_at" INTEGER NOT NULL,
|
||||
"created_at" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "idx_oidc_auth_codes_expires_at" ON "oidc_authorization_codes"("expires_at");
|
||||
|
||||
@@ -11,11 +11,10 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/model"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/controller"
|
||||
"tinyauth/internal/model"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -3,9 +3,8 @@ package bootstrap
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/middleware"
|
||||
"tinyauth/internal/controller"
|
||||
"tinyauth/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -102,15 +101,5 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||
|
||||
healthController.SetupRoutes()
|
||||
|
||||
// Setup OIDC controller if OIDC is enabled
|
||||
if app.config.OIDC.Enabled && app.services.oidcService != nil {
|
||||
oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{
|
||||
AppURL: app.config.AppURL,
|
||||
CookieDomain: app.context.cookieDomain,
|
||||
}, apiRouter, app.services.oidcService, app.services.authService)
|
||||
|
||||
oidcController.SetupRoutes()
|
||||
}
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"tinyauth/internal/service"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -13,7 +13,6 @@ type Services struct {
|
||||
dockerService *service.DockerService
|
||||
ldapService *service.LdapService
|
||||
oauthBrokerService *service.OAuthBrokerService
|
||||
oidcService *service.OIDCService
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) initServices() (Services, error) {
|
||||
@@ -97,39 +96,5 @@ func (app *BootstrapApp) initServices() (Services, error) {
|
||||
|
||||
services.oauthBrokerService = oauthBrokerService
|
||||
|
||||
// Initialize OIDC service if enabled
|
||||
if app.config.OIDC.Enabled {
|
||||
issuer := app.config.OIDC.Issuer
|
||||
if issuer == "" {
|
||||
issuer = app.config.AppURL
|
||||
}
|
||||
|
||||
oidcService := service.NewOIDCService(service.OIDCServiceConfig{
|
||||
AppURL: app.config.AppURL,
|
||||
Issuer: issuer,
|
||||
AccessTokenExpiry: app.config.OIDC.AccessTokenExpiry,
|
||||
IDTokenExpiry: app.config.OIDC.IDTokenExpiry,
|
||||
Database: databaseService.GetDatabase(),
|
||||
})
|
||||
|
||||
err = oidcService.Init()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to initialize OIDC service, continuing without it")
|
||||
} else {
|
||||
services.oidcService = oidcService
|
||||
log.Info().Msg("OIDC service initialized")
|
||||
|
||||
// Sync clients from config
|
||||
if len(app.config.OIDC.Clients) > 0 {
|
||||
err = oidcService.SyncClientsFromConfig(app.config.OIDC.Clients)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to sync OIDC clients from config")
|
||||
} else {
|
||||
log.Info().Int("count", len(app.config.OIDC.Clients)).Msg("Synced OIDC clients from config")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ type Config struct {
|
||||
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
||||
OIDC OIDCConfig `description:"OIDC provider configuration." yaml:"oidc"`
|
||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||
@@ -69,24 +68,6 @@ type LdapConfig struct {
|
||||
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
Enabled bool `description:"Enable OIDC provider functionality." yaml:"enabled"`
|
||||
Issuer string `description:"OIDC issuer URL (defaults to appUrl)." yaml:"issuer"`
|
||||
AccessTokenExpiry int `description:"Access token expiry time in seconds." yaml:"accessTokenExpiry"`
|
||||
IDTokenExpiry int `description:"ID token expiry time in seconds." yaml:"idTokenExpiry"`
|
||||
Clients map[string]OIDCClientConfig `description:"OIDC client configurations." yaml:"clients"`
|
||||
}
|
||||
|
||||
type OIDCClientConfig struct {
|
||||
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret"`
|
||||
ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile"`
|
||||
ClientName string `description:"Client name for display purposes." yaml:"clientName"`
|
||||
RedirectURIs []string `description:"Allowed redirect URIs." yaml:"redirectUris"`
|
||||
GrantTypes []string `description:"Allowed grant types (defaults to ['authorization_code'])." yaml:"grantTypes"`
|
||||
ResponseTypes []string `description:"Allowed response types (defaults to ['code'])." yaml:"responseTypes"`
|
||||
Scopes []string `description:"Allowed scopes (defaults to ['openid', 'profile', 'email'])." yaml:"scopes"`
|
||||
}
|
||||
|
||||
type ExperimentalConfig struct {
|
||||
ConfigFile string `description:"Path to config file." yaml:"-"`
|
||||
}
|
||||
@@ -98,7 +79,6 @@ const DefaultNamePrefix = "TINYAUTH_"
|
||||
// OAuth/OIDC config
|
||||
|
||||
type Claims struct {
|
||||
Sub string `json:"sub"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
@@ -145,7 +125,6 @@ type SessionCookie struct {
|
||||
TotpPending bool
|
||||
OAuthGroups string
|
||||
OAuthName string
|
||||
OAuthSub string
|
||||
}
|
||||
|
||||
type UserContext struct {
|
||||
@@ -159,7 +138,6 @@ type UserContext struct {
|
||||
OAuthGroups string
|
||||
TotpEnabled bool
|
||||
OAuthName string
|
||||
OAuthSub string
|
||||
}
|
||||
|
||||
// API responses and queries
|
||||
|
||||
@@ -3,8 +3,7 @@ package controller
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -21,7 +20,6 @@ type UserContextResponse struct {
|
||||
OAuth bool `json:"oauth"`
|
||||
TotpPending bool `json:"totpPending"`
|
||||
OAuthName string `json:"oauthName"`
|
||||
OAuthSub string `json:"oauthSub"`
|
||||
}
|
||||
|
||||
type AppContextResponse struct {
|
||||
@@ -90,7 +88,6 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||
OAuth: context.OAuth,
|
||||
TotpPending: context.TotpPending,
|
||||
OAuthName: context.OAuthName,
|
||||
OAuthSub: context.OAuthSub,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/controller"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
@@ -44,7 +43,6 @@ var userContext = config.UserContext{
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: false,
|
||||
OAuthSub: "",
|
||||
}
|
||||
|
||||
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||
|
||||
@@ -5,10 +5,9 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/service"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-querystring/query"
|
||||
@@ -197,7 +196,6 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
Provider: req.Provider,
|
||||
OAuthGroups: utils.CoalesceToString(user.Groups),
|
||||
OAuthName: service.GetName(),
|
||||
OAuthSub: user.Sub,
|
||||
}
|
||||
|
||||
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||
|
||||
@@ -1,489 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// OIDCControllerConfig holds configuration for the OIDC controller.
|
||||
type OIDCControllerConfig struct {
|
||||
AppURL string // Base URL of the application
|
||||
CookieDomain string // Domain for setting cookies
|
||||
}
|
||||
|
||||
// OIDCController handles OpenID Connect (OIDC) protocol endpoints.
|
||||
// It implements the OIDC provider functionality including discovery, authorization,
|
||||
// token exchange, userinfo, and JWKS endpoints.
|
||||
type OIDCController struct {
|
||||
config OIDCControllerConfig
|
||||
router *gin.RouterGroup
|
||||
oidc *service.OIDCService
|
||||
auth *service.AuthService
|
||||
}
|
||||
|
||||
// NewOIDCController creates a new OIDC controller with the given configuration and services.
|
||||
func NewOIDCController(config OIDCControllerConfig, router *gin.RouterGroup, oidc *service.OIDCService, auth *service.AuthService) *OIDCController {
|
||||
return &OIDCController{
|
||||
config: config,
|
||||
router: router,
|
||||
oidc: oidc,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
// SetupRoutes registers all OIDC endpoints with the router.
|
||||
// This includes:
|
||||
// - /.well-known/openid-configuration - OIDC discovery endpoint
|
||||
// - /oidc/authorize - Authorization endpoint
|
||||
// - /oidc/token - Token endpoint
|
||||
// - /oidc/userinfo - UserInfo endpoint
|
||||
// - /oidc/jwks - JSON Web Key Set endpoint
|
||||
func (controller *OIDCController) SetupRoutes() {
|
||||
// Well-known discovery endpoint
|
||||
controller.router.GET("/.well-known/openid-configuration", controller.discoveryHandler)
|
||||
|
||||
// OIDC endpoints
|
||||
oidcGroup := controller.router.Group("/oidc")
|
||||
oidcGroup.GET("/authorize", controller.authorizeHandler)
|
||||
oidcGroup.POST("/token", controller.tokenHandler)
|
||||
oidcGroup.GET("/userinfo", controller.userinfoHandler)
|
||||
oidcGroup.GET("/jwks", controller.jwksHandler)
|
||||
}
|
||||
|
||||
// discoveryHandler handles the OIDC discovery endpoint.
|
||||
// Returns the OpenID Connect discovery document as specified in RFC 8414.
|
||||
// The document contains metadata about the OIDC provider including endpoints,
|
||||
// supported features, and cryptographic capabilities.
|
||||
func (controller *OIDCController) discoveryHandler(c *gin.Context) {
|
||||
issuer := controller.oidc.GetIssuer()
|
||||
baseURL := strings.TrimSuffix(controller.config.AppURL, "/")
|
||||
|
||||
discovery := map[string]interface{}{
|
||||
"issuer": issuer,
|
||||
"authorization_endpoint": fmt.Sprintf("%s/api/oidc/authorize", baseURL),
|
||||
"token_endpoint": fmt.Sprintf("%s/api/oidc/token", baseURL),
|
||||
"userinfo_endpoint": fmt.Sprintf("%s/api/oidc/userinfo", baseURL),
|
||||
"jwks_uri": fmt.Sprintf("%s/api/oidc/jwks", baseURL),
|
||||
"response_types_supported": []string{"code"},
|
||||
"subject_types_supported": []string{"public"},
|
||||
"id_token_signing_alg_values_supported": []string{"RS256"},
|
||||
"scopes_supported": []string{"openid", "profile", "email"},
|
||||
"token_endpoint_auth_methods_supported": []string{"client_secret_basic", "client_secret_post"},
|
||||
"grant_types_supported": []string{"authorization_code"},
|
||||
"code_challenge_methods_supported": []string{"S256", "plain"},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, discovery)
|
||||
}
|
||||
|
||||
// authorizeHandler handles the OIDC authorization endpoint.
|
||||
// Implements the authorization code flow as specified in OAuth 2.0 RFC 6749.
|
||||
// Validates client credentials, redirect URI, scopes, and response type.
|
||||
// Supports PKCE (RFC 7636) for enhanced security.
|
||||
// If the user is not authenticated, redirects to the login page with the
|
||||
// authorization request parameters preserved for redirect after login.
|
||||
// On success, generates an authorization code and redirects to the client's
|
||||
// redirect URI with the code and state parameter.
|
||||
func (controller *OIDCController) authorizeHandler(c *gin.Context) {
|
||||
// Get query parameters
|
||||
clientID := c.Query("client_id")
|
||||
redirectURI := c.Query("redirect_uri")
|
||||
responseType := c.Query("response_type")
|
||||
scope := c.Query("scope")
|
||||
state := c.Query("state")
|
||||
nonce := c.Query("nonce")
|
||||
codeChallenge := c.Query("code_challenge")
|
||||
codeChallengeMethod := c.Query("code_challenge_method")
|
||||
|
||||
// Validate required parameters
|
||||
// Return JSON error instead of redirecting since redirect_uri is not yet validated
|
||||
if clientID == "" || redirectURI == "" || responseType == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Missing required parameters",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get client
|
||||
// Return JSON error instead of redirecting since redirect_uri is not yet validated
|
||||
client, err := controller.oidc.GetClient(clientID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_client",
|
||||
"error_description": "Client not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate redirect URI
|
||||
// After this point, redirect_uri is validated and we can safely redirect
|
||||
if !controller.oidc.ValidateRedirectURI(client, redirectURI) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Invalid redirect_uri",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate response type
|
||||
if !controller.oidc.ValidateResponseType(client, responseType) {
|
||||
controller.redirectError(c, redirectURI, state, "unsupported_response_type", "Unsupported response_type")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate scopes
|
||||
scopes, err := controller.oidc.ValidateScope(client, scope)
|
||||
if err != nil {
|
||||
controller.redirectError(c, redirectURI, state, "invalid_scope", "Invalid scope")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
userContext, err := utils.GetContext(c)
|
||||
if err != nil || !userContext.IsLoggedIn {
|
||||
// User not authenticated, redirect to login
|
||||
// Build the full authorize URL to redirect back to after login
|
||||
authorizeURL := fmt.Sprintf("%s%s", controller.config.AppURL, c.Request.URL.Path)
|
||||
if c.Request.URL.RawQuery != "" {
|
||||
authorizeURL = fmt.Sprintf("%s?%s", authorizeURL, c.Request.URL.RawQuery)
|
||||
}
|
||||
loginURL := fmt.Sprintf("%s/login?redirect_uri=%s&client_id=%s&response_type=%s&scope=%s&state=%s&nonce=%s&code_challenge=%s&code_challenge_method=%s",
|
||||
controller.config.AppURL,
|
||||
url.QueryEscape(authorizeURL),
|
||||
url.QueryEscape(clientID),
|
||||
url.QueryEscape(responseType),
|
||||
url.QueryEscape(scope),
|
||||
url.QueryEscape(state),
|
||||
url.QueryEscape(nonce),
|
||||
url.QueryEscape(codeChallenge),
|
||||
url.QueryEscape(codeChallengeMethod))
|
||||
c.Redirect(http.StatusFound, loginURL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for TOTP pending
|
||||
if userContext.TotpPending {
|
||||
controller.redirectError(c, redirectURI, state, "access_denied", "TOTP verification required")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate authorization code (including PKCE challenge if provided)
|
||||
authCode, err := controller.oidc.GenerateAuthorizationCode(&userContext, clientID, redirectURI, scopes, nonce, codeChallenge, codeChallengeMethod)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate authorization code")
|
||||
controller.redirectError(c, redirectURI, state, "server_error", "Internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
// Build redirect URL with authorization code
|
||||
redirectURL, err := url.Parse(redirectURI)
|
||||
if err != nil {
|
||||
controller.redirectError(c, redirectURI, state, "invalid_request", "Invalid redirect_uri")
|
||||
return
|
||||
}
|
||||
|
||||
query := redirectURL.Query()
|
||||
query.Set("code", authCode)
|
||||
if state != "" {
|
||||
query.Set("state", state)
|
||||
}
|
||||
redirectURL.RawQuery = query.Encode()
|
||||
|
||||
c.Redirect(http.StatusFound, redirectURL.String())
|
||||
}
|
||||
|
||||
// tokenHandler handles the OIDC token endpoint.
|
||||
// Exchanges an authorization code for access and ID tokens.
|
||||
// Validates the authorization code, client credentials, redirect URI, and PKCE verifier.
|
||||
// Returns an access token and optionally an ID token (if openid scope is present).
|
||||
// Implements the authorization code grant type as specified in OAuth 2.0 RFC 6749.
|
||||
func (controller *OIDCController) tokenHandler(c *gin.Context) {
|
||||
// Get grant type
|
||||
grantType := c.PostForm("grant_type")
|
||||
if grantType == "" {
|
||||
grantType = c.Query("grant_type")
|
||||
}
|
||||
|
||||
if grantType != "authorization_code" {
|
||||
controller.tokenError(c, "unsupported_grant_type", "Only authorization_code grant type is supported")
|
||||
return
|
||||
}
|
||||
|
||||
// Get authorization code
|
||||
code := c.PostForm("code")
|
||||
if code == "" {
|
||||
code = c.Query("code")
|
||||
}
|
||||
|
||||
if code == "" {
|
||||
controller.tokenError(c, "invalid_request", "Missing authorization code")
|
||||
return
|
||||
}
|
||||
|
||||
// Get client credentials
|
||||
clientID, clientSecret, err := controller.getClientCredentials(c)
|
||||
if err != nil {
|
||||
controller.tokenError(c, "invalid_client", "Invalid client credentials")
|
||||
return
|
||||
}
|
||||
|
||||
// Get client
|
||||
client, err := controller.oidc.GetClient(clientID)
|
||||
if err != nil {
|
||||
controller.tokenError(c, "invalid_client", "Client not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify client secret
|
||||
if !controller.oidc.VerifyClientSecret(client, clientSecret) {
|
||||
controller.tokenError(c, "invalid_client", "Invalid client secret")
|
||||
return
|
||||
}
|
||||
|
||||
// Get redirect URI
|
||||
redirectURI := c.PostForm("redirect_uri")
|
||||
if redirectURI == "" {
|
||||
redirectURI = c.Query("redirect_uri")
|
||||
}
|
||||
|
||||
// Validate redirect URI
|
||||
if !controller.oidc.ValidateRedirectURI(client, redirectURI) {
|
||||
controller.tokenError(c, "invalid_request", "Invalid redirect_uri")
|
||||
return
|
||||
}
|
||||
|
||||
// Get code_verifier for PKCE validation
|
||||
codeVerifier := c.PostForm("code_verifier")
|
||||
if codeVerifier == "" {
|
||||
codeVerifier = c.Query("code_verifier")
|
||||
}
|
||||
|
||||
// Validate authorization code
|
||||
userContext, scopes, nonce, codeChallenge, codeChallengeMethod, err := controller.oidc.ValidateAuthorizationCode(code, clientID, redirectURI)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to validate authorization code")
|
||||
controller.tokenError(c, "invalid_grant", "Invalid or expired authorization code")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate PKCE if code challenge was provided
|
||||
if codeChallenge != "" {
|
||||
if err := controller.oidc.ValidatePKCE(codeChallenge, codeChallengeMethod, codeVerifier); err != nil {
|
||||
log.Error().Err(err).Msg("PKCE validation failed")
|
||||
controller.tokenError(c, "invalid_grant", "Invalid code_verifier")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, err := controller.oidc.GenerateAccessToken(userContext, clientID, scopes)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate access token")
|
||||
controller.tokenError(c, "server_error", "Internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate ID token if openid scope is present
|
||||
var idToken string
|
||||
hasOpenID := false
|
||||
for _, scope := range scopes {
|
||||
if scope == "openid" {
|
||||
hasOpenID = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasOpenID {
|
||||
idToken, err = controller.oidc.GenerateIDToken(userContext, clientID, nonce)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate ID token")
|
||||
controller.tokenError(c, "server_error", "Internal server error")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Return token response
|
||||
response := map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": controller.oidc.GetAccessTokenExpiry(),
|
||||
"scope": strings.Join(scopes, " "),
|
||||
}
|
||||
|
||||
if idToken != "" {
|
||||
response["id_token"] = idToken
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// userinfoHandler handles the OIDC UserInfo endpoint.
|
||||
// Returns user information claims for the authenticated user based on the
|
||||
// provided access token. Validates the access token signature, issuer, and expiration.
|
||||
// Returns standard OIDC claims: sub, email, name, and preferred_username.
|
||||
func (controller *OIDCController) userinfoHandler(c *gin.Context) {
|
||||
// Get access token from Authorization header or query parameter
|
||||
accessToken := controller.getAccessToken(c)
|
||||
if accessToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "invalid_token",
|
||||
"error_description": "Missing access token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get optional client_id from request for audience validation
|
||||
clientID := c.Query("client_id")
|
||||
if clientID == "" {
|
||||
clientID = c.PostForm("client_id")
|
||||
}
|
||||
|
||||
// Validate and parse access token with audience validation
|
||||
userContext, err := controller.oidc.ValidateAccessTokenForClient(accessToken, clientID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to validate access token")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "invalid_token",
|
||||
"error_description": "Invalid or expired access token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Return user info
|
||||
userInfo := map[string]interface{}{
|
||||
"sub": userContext.Username,
|
||||
"email": userContext.Email,
|
||||
"name": userContext.Name,
|
||||
"preferred_username": userContext.Username,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, userInfo)
|
||||
}
|
||||
|
||||
// jwksHandler handles the JSON Web Key Set (JWKS) endpoint.
|
||||
// Returns the public keys used to verify ID tokens and access tokens.
|
||||
// The keys are in JWK format as specified in RFC 7517.
|
||||
func (controller *OIDCController) jwksHandler(c *gin.Context) {
|
||||
jwks, err := controller.oidc.GetJWKS()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWKS")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "server_error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, jwks)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// redirectError redirects the user to the redirect URI with an error response.
|
||||
// Includes the error code, error description, and state parameter (if provided).
|
||||
// If the redirect URI is invalid or empty, returns a JSON error response instead.
|
||||
func (controller *OIDCController) redirectError(c *gin.Context, redirectURI string, state string, errorCode string, errorDescription string) {
|
||||
if redirectURI == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": errorCode,
|
||||
"error_description": errorDescription,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := url.Parse(redirectURI)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": errorCode,
|
||||
"error_description": errorDescription,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
query := redirectURL.Query()
|
||||
query.Set("error", errorCode)
|
||||
query.Set("error_description", errorDescription)
|
||||
if state != "" {
|
||||
query.Set("state", state)
|
||||
}
|
||||
redirectURL.RawQuery = query.Encode()
|
||||
|
||||
c.Redirect(http.StatusFound, redirectURL.String())
|
||||
}
|
||||
|
||||
// tokenError returns a JSON error response for token endpoint errors.
|
||||
// Uses the standard OAuth 2.0 error format with error and error_description fields.
|
||||
func (controller *OIDCController) tokenError(c *gin.Context, errorCode string, errorDescription string) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": errorCode,
|
||||
"error_description": errorDescription,
|
||||
})
|
||||
}
|
||||
|
||||
// getClientCredentials extracts client credentials from the request.
|
||||
// Supports client_secret_basic (HTTP Basic Authentication) and
|
||||
// client_secret_post (POST form parameters) as specified in the discovery document.
|
||||
// Does not accept credentials via query parameters for security reasons
|
||||
// (they may be logged in access logs, browser history, or referrer headers).
|
||||
// Returns the client ID, client secret, and an error if credentials are not found.
|
||||
func (controller *OIDCController) getClientCredentials(c *gin.Context) (string, string, error) {
|
||||
// Try Basic Auth first (client_secret_basic)
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if strings.HasPrefix(authHeader, "Basic ") {
|
||||
encoded := strings.TrimPrefix(authHeader, "Basic ")
|
||||
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err == nil {
|
||||
parts := strings.SplitN(string(decoded), ":", 2)
|
||||
if len(parts) == 2 {
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try POST form parameters (client_secret_post)
|
||||
clientID := c.PostForm("client_id")
|
||||
clientSecret := c.PostForm("client_secret")
|
||||
if clientID != "" && clientSecret != "" {
|
||||
return clientID, clientSecret, nil
|
||||
}
|
||||
|
||||
// Do not accept credentials via query parameters as they are logged
|
||||
// in access logs, browser history, and referrer headers
|
||||
return "", "", fmt.Errorf("client credentials not found")
|
||||
}
|
||||
|
||||
// getAccessToken extracts the access token from the request.
|
||||
// Checks the Authorization header (Bearer token) first, then falls back to
|
||||
// the access_token query parameter.
|
||||
// Returns an empty string if no access token is found.
|
||||
func (controller *OIDCController) getAccessToken(c *gin.Context) string {
|
||||
// Try Authorization header
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return strings.TrimPrefix(authHeader, "Bearer ")
|
||||
}
|
||||
|
||||
// Try query parameter
|
||||
return c.Query("access_token")
|
||||
}
|
||||
|
||||
// validateAccessToken validates an access token and extracts user context.
|
||||
// Verifies the JWT signature using the OIDC service's public key, checks the
|
||||
// issuer, and validates expiration. Returns the user context if valid, or an
|
||||
// error if validation fails.
|
||||
func (controller *OIDCController) validateAccessToken(accessToken string) (*config.UserContext, error) {
|
||||
// Validate the JWT token using the OIDC service's public key
|
||||
// This properly verifies the signature, issuer, and expiration
|
||||
// Note: This method does not validate audience - use ValidateAccessTokenForClient for that
|
||||
return controller.oidc.ValidateAccessToken(accessToken)
|
||||
}
|
||||
@@ -3,20 +3,16 @@ package controller
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/service"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var SupportedProxies = []string{"nginx", "traefik", "caddy", "envoy"}
|
||||
|
||||
type Proxy struct {
|
||||
Proxy string `uri:"proxy" binding:"required"`
|
||||
}
|
||||
@@ -44,7 +40,6 @@ func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, a
|
||||
func (controller *ProxyController) SetupRoutes() {
|
||||
proxyGroup := controller.router.Group("/auth")
|
||||
proxyGroup.GET("/:proxy", controller.proxyHandler)
|
||||
proxyGroup.POST("/:proxy", controller.proxyHandler)
|
||||
}
|
||||
|
||||
func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
@@ -60,7 +55,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !slices.Contains(SupportedProxies, req.Proxy) {
|
||||
if req.Proxy != "nginx" && req.Proxy != "traefik" && req.Proxy != "caddy" {
|
||||
log.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
@@ -239,7 +234,6 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
|
||||
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
|
||||
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
|
||||
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuthSub))
|
||||
|
||||
controller.setHeaders(c, acls)
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@ package controller_test
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/controller"
|
||||
"tinyauth/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
@@ -93,18 +92,6 @@ func TestProxyHandler(t *testing.T) {
|
||||
assert.Equal(t, 307, recorder.Code)
|
||||
assert.Equal(t, "http://localhost:8080/login?redirect_uri=https%3A%2F%2Fexample.com%2Fsomepath", recorder.Header().Get("Location"))
|
||||
|
||||
// Test logged out user (envoy)
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/auth/envoy", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
req.Header.Set("X-Forwarded-Host", "example.com")
|
||||
req.Header.Set("X-Forwarded-Uri", "/somepath")
|
||||
req.Header.Set("Accept", "text/html")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 307, recorder.Code)
|
||||
assert.Equal(t, "http://localhost:8080/login?redirect_uri=https%3A%2F%2Fexample.com%2Fsomepath", recorder.Header().Get("Location"))
|
||||
|
||||
// Test logged out user (nginx)
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
|
||||
@@ -4,8 +4,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"tinyauth/internal/controller"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
@@ -3,10 +3,9 @@ package controller
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/service"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/controller"
|
||||
"tinyauth/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
@@ -3,10 +3,9 @@ package middleware
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/service"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -66,7 +65,6 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
goto basic
|
||||
}
|
||||
|
||||
m.auth.RefreshSessionCookie(c)
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: cookie.Username,
|
||||
Name: cookie.Name,
|
||||
@@ -91,7 +89,6 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
goto basic
|
||||
}
|
||||
|
||||
m.auth.RefreshSessionCookie(c)
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: cookie.Username,
|
||||
Name: cookie.Name,
|
||||
@@ -99,7 +96,6 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
Provider: cookie.Provider,
|
||||
OAuthGroups: cookie.OAuthGroups,
|
||||
OAuthName: cookie.OAuthName,
|
||||
OAuthSub: cookie.OAuthSub,
|
||||
IsLoggedIn: true,
|
||||
OAuth: true,
|
||||
})
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/assets"
|
||||
"tinyauth/internal/assets"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package model
|
||||
|
||||
type OIDCAuthorizationCode struct {
|
||||
Code string `gorm:"column:code;primaryKey"`
|
||||
ClientID string `gorm:"column:client_id;not null"`
|
||||
RedirectURI string `gorm:"column:redirect_uri;not null"`
|
||||
Used bool `gorm:"column:used;default:false"`
|
||||
ExpiresAt int64 `gorm:"column:expires_at;not null"`
|
||||
CreatedAt int64 `gorm:"column:created_at;not null"`
|
||||
}
|
||||
|
||||
func (OIDCAuthorizationCode) TableName() string {
|
||||
return "oidc_authorization_codes"
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package model
|
||||
|
||||
type OIDCClient struct {
|
||||
ClientID string `gorm:"column:client_id;primaryKey"`
|
||||
ClientSecret string `gorm:"column:client_secret"`
|
||||
ClientName string `gorm:"column:client_name"`
|
||||
RedirectURIs string `gorm:"column:redirect_uris"` // JSON array
|
||||
GrantTypes string `gorm:"column:grant_types"` // JSON array
|
||||
ResponseTypes string `gorm:"column:response_types"` // JSON array
|
||||
Scopes string `gorm:"column:scopes"` // JSON array
|
||||
CreatedAt int64 `gorm:"column:created_at"`
|
||||
UpdatedAt int64 `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
func (OIDCClient) TableName() string {
|
||||
return "oidc_clients"
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package model
|
||||
|
||||
type OIDCKey struct {
|
||||
ID int `gorm:"column:id;primaryKey;autoIncrement"`
|
||||
PrivateKey string `gorm:"column:private_key;not null"`
|
||||
CreatedAt int64 `gorm:"column:created_at"`
|
||||
UpdatedAt int64 `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
func (OIDCKey) TableName() string {
|
||||
return "oidc_keys"
|
||||
}
|
||||
|
||||
@@ -10,5 +10,4 @@ type Session struct {
|
||||
OAuthGroups string `gorm:"column:oauth_groups"`
|
||||
Expiry int64 `gorm:"column:expiry"`
|
||||
OAuthName string `gorm:"column:oauth_name"`
|
||||
OAuthSub string `gorm:"column:oauth_sub"`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"tinyauth/internal/config"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/model"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/model"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
@@ -43,6 +43,7 @@ type AuthService struct {
|
||||
loginMutex sync.RWMutex
|
||||
ldap *LdapService
|
||||
database *gorm.DB
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, database *gorm.DB) *AuthService {
|
||||
@@ -56,6 +57,7 @@ func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapS
|
||||
}
|
||||
|
||||
func (auth *AuthService) Init() error {
|
||||
auth.ctx = context.Background()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -213,10 +215,9 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio
|
||||
OAuthGroups: data.OAuthGroups,
|
||||
Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
|
||||
OAuthName: data.OAuthName,
|
||||
OAuthSub: data.OAuthSub,
|
||||
}
|
||||
|
||||
err = gorm.G[model.Session](auth.database).Create(c, &session)
|
||||
err = gorm.G[model.Session](auth.database).Create(auth.ctx, &session)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -227,40 +228,6 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
||||
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).First(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentTime := time.Now().Unix()
|
||||
|
||||
if session.Expiry-currentTime > int64(time.Hour.Seconds()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
newExpiry := currentTime + int64(time.Hour.Seconds())
|
||||
|
||||
_, err = gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).Updates(c, model.Session{
|
||||
Expiry: newExpiry,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.SetCookie(auth.config.SessionCookieName, cookie, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
|
||||
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
||||
|
||||
@@ -268,7 +235,7 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).Delete(c)
|
||||
_, err = gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).Delete(auth.ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -286,7 +253,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
||||
return config.SessionCookie{}, err
|
||||
}
|
||||
|
||||
session, err := gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).First(c)
|
||||
session, err := gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).First(auth.ctx)
|
||||
|
||||
if err != nil {
|
||||
return config.SessionCookie{}, err
|
||||
@@ -299,7 +266,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
||||
currentTime := time.Now().Unix()
|
||||
|
||||
if currentTime > session.Expiry {
|
||||
_, err = gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).Delete(c)
|
||||
_, err = gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).Delete(auth.ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to delete expired session")
|
||||
}
|
||||
@@ -315,7 +282,6 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
||||
TotpPending: session.TOTPPending,
|
||||
OAuthGroups: session.OAuthGroups,
|
||||
OAuthName: session.OAuthName,
|
||||
OAuthSub: session.OAuthSub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/assets"
|
||||
"tinyauth/internal/assets"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
|
||||
@@ -3,9 +3,8 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/decoders"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils/decoders"
|
||||
|
||||
container "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
@@ -10,8 +10,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
@@ -9,10 +9,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/endpoints"
|
||||
@@ -28,7 +26,6 @@ type GithubEmailResponse []struct {
|
||||
type GithubUserInfoResponse struct {
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
type GithubOAuthService struct {
|
||||
@@ -174,7 +171,6 @@ func (github *GithubOAuthService) Userinfo() (config.Claims, error) {
|
||||
|
||||
user.PreferredUsername = userInfo.Login
|
||||
user.Name = userInfo.Name
|
||||
user.Sub = strconv.Itoa(userInfo.ID)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -10,14 +10,18 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/endpoints"
|
||||
)
|
||||
|
||||
var GoogleOAuthScopes = []string{"openid", "email", "profile"}
|
||||
var GoogleOAuthScopes = []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}
|
||||
|
||||
type GoogleUserInfoResponse struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type GoogleOAuthService struct {
|
||||
config oauth2.Config
|
||||
@@ -86,7 +90,7 @@ func (google *GoogleOAuthService) Userinfo() (config.Claims, error) {
|
||||
|
||||
client := google.config.Client(google.context, google.token)
|
||||
|
||||
res, err := client.Get("https://openidconnect.googleapis.com/v1/userinfo")
|
||||
res, err := client.Get("https://www.googleapis.com/userinfo/v2/me")
|
||||
if err != nil {
|
||||
return config.Claims{}, err
|
||||
}
|
||||
@@ -101,12 +105,16 @@ func (google *GoogleOAuthService) Userinfo() (config.Claims, error) {
|
||||
return config.Claims{}, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &user)
|
||||
var userInfo GoogleUserInfoResponse
|
||||
|
||||
err = json.Unmarshal(body, &userInfo)
|
||||
if err != nil {
|
||||
return config.Claims{}, err
|
||||
}
|
||||
|
||||
user.PreferredUsername = strings.SplitN(user.Email, "@", 2)[0]
|
||||
user.PreferredUsername = strings.Split(userInfo.Email, "@")[0]
|
||||
user.Name = userInfo.Name
|
||||
user.Email = userInfo.Email
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
@@ -1,822 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/model"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OIDCServiceConfig struct {
|
||||
AppURL string
|
||||
Issuer string
|
||||
AccessTokenExpiry int
|
||||
IDTokenExpiry int
|
||||
Database *gorm.DB
|
||||
}
|
||||
|
||||
type OIDCService struct {
|
||||
config OIDCServiceConfig
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey *rsa.PublicKey
|
||||
masterKey []byte // Master key for encrypting private keys (optional)
|
||||
}
|
||||
|
||||
func NewOIDCService(config OIDCServiceConfig) *OIDCService {
|
||||
return &OIDCService{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// encryptPrivateKey encrypts a private key PEM string using AES-GCM
|
||||
func (oidc *OIDCService) encryptPrivateKey(plaintext string) (string, error) {
|
||||
if len(oidc.masterKey) == 0 {
|
||||
// No encryption key set, return plaintext
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// Derive AES-256 key from master key using HKDF
|
||||
hkdfReader := hkdf.New(sha256.New, oidc.masterKey, nil, []byte("oidc-aes-256-key-v1"))
|
||||
key := make([]byte, 32) // AES-256 requires 32 bytes
|
||||
if _, err := io.ReadFull(hkdfReader, key); err != nil {
|
||||
return "", fmt.Errorf("failed to derive encryption key: %w", err)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return "", fmt.Errorf("failed to generate nonce: %w", err)
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
// Encode as base64 for storage
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// decryptPrivateKey decrypts an encrypted private key PEM string
|
||||
func (oidc *OIDCService) decryptPrivateKey(encrypted string) (string, error) {
|
||||
if len(oidc.masterKey) == 0 {
|
||||
// No encryption key set, assume plaintext
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// Try to decode as base64 (encrypted) first
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
// Not base64, assume it's plaintext (backward compatibility)
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// Derive AES-256 key from master key using HKDF
|
||||
hkdfReader := hkdf.New(sha256.New, oidc.masterKey, nil, []byte("oidc-aes-256-key-v1"))
|
||||
key := make([]byte, 32) // AES-256 requires 32 bytes
|
||||
if _, err := io.ReadFull(hkdfReader, key); err != nil {
|
||||
return "", fmt.Errorf("failed to derive decryption key: %w", err)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(ciphertext) < nonceSize {
|
||||
// Too short to be encrypted, assume plaintext
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decrypt private key: %w", err)
|
||||
}
|
||||
|
||||
return string(plaintext), nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) Init() error {
|
||||
// Load master key from environment (optional)
|
||||
masterKeyEnv := os.Getenv("OIDC_RSA_MASTER_KEY")
|
||||
if masterKeyEnv != "" {
|
||||
oidc.masterKey = []byte(masterKeyEnv)
|
||||
if len(oidc.masterKey) < 32 {
|
||||
log.Warn().Msg("OIDC_RSA_MASTER_KEY is shorter than 32 bytes, consider using a longer key for better security")
|
||||
}
|
||||
log.Info().Msg("RSA private key encryption enabled (using OIDC_RSA_MASTER_KEY)")
|
||||
} else {
|
||||
log.Info().Msg("RSA private key encryption disabled (OIDC_RSA_MASTER_KEY not set)")
|
||||
}
|
||||
// Check if multiple keys exist (for warning)
|
||||
var keyCount int64
|
||||
if err := oidc.config.Database.Model(&model.OIDCKey{}).Count(&keyCount).Error; err != nil {
|
||||
return fmt.Errorf("failed to count RSA keys: %w", err)
|
||||
}
|
||||
if keyCount > 1 {
|
||||
log.Warn().Int64("count", keyCount).Msg("Multiple RSA keys detected in database, loading most recently created key. Consider cleaning up older keys.")
|
||||
}
|
||||
|
||||
// Try to load existing key from database (most recently created)
|
||||
var keyRecord model.OIDCKey
|
||||
err := oidc.config.Database.Order("created_at DESC").First(&keyRecord).Error
|
||||
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("failed to query for existing RSA key: %w", err)
|
||||
}
|
||||
|
||||
var privateKey *rsa.PrivateKey
|
||||
|
||||
if err == nil && keyRecord.PrivateKey != "" {
|
||||
// Decrypt private key if encrypted
|
||||
privateKeyPEM, err := oidc.decryptPrivateKey(keyRecord.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt private key: %w", err)
|
||||
}
|
||||
|
||||
// Load existing key
|
||||
block, _ := pem.Decode([]byte(privateKeyPEM))
|
||||
if block == nil {
|
||||
return fmt.Errorf("failed to decode PEM block from stored key")
|
||||
}
|
||||
|
||||
parsedKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
// Try PKCS8 format as fallback
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse stored private key: %w", err)
|
||||
}
|
||||
var ok bool
|
||||
privateKey, ok = key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("stored key is not an RSA private key")
|
||||
}
|
||||
} else {
|
||||
privateKey = parsedKey
|
||||
}
|
||||
|
||||
oidc.privateKey = privateKey
|
||||
oidc.publicKey = &privateKey.PublicKey
|
||||
|
||||
log.Info().Msg("OIDC service initialized with existing RSA key pair from database")
|
||||
return nil
|
||||
}
|
||||
|
||||
// No existing key found, generate new one
|
||||
privateKey, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate RSA key: %w", err)
|
||||
}
|
||||
|
||||
// Encode private key to PEM format
|
||||
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: privateKeyBytes,
|
||||
})
|
||||
|
||||
// Encrypt private key before storing
|
||||
encryptedPrivateKey, err := oidc.encryptPrivateKey(string(privateKeyPEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt private key: %w", err)
|
||||
}
|
||||
|
||||
// Save to database
|
||||
now := time.Now().Unix()
|
||||
keyRecord = model.OIDCKey{
|
||||
PrivateKey: encryptedPrivateKey,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := oidc.config.Database.Create(&keyRecord).Error; err != nil {
|
||||
return fmt.Errorf("failed to save RSA key to database: %w", err)
|
||||
}
|
||||
|
||||
oidc.privateKey = privateKey
|
||||
oidc.publicKey = &privateKey.PublicKey
|
||||
|
||||
log.Info().Msg("OIDC service initialized with new RSA key pair (saved to database)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) GetClient(clientID string) (*model.OIDCClient, error) {
|
||||
var client model.OIDCClient
|
||||
err := oidc.config.Database.Where("client_id = ?", clientID).First(&client).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("client not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) VerifyClientSecret(client *model.OIDCClient, secret string) bool {
|
||||
// Use bcrypt for constant-time comparison to prevent timing attacks
|
||||
err := bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(secret))
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Str("client_id", client.ClientID).Msg("Client secret verification failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) ValidateRedirectURI(client *model.OIDCClient, redirectURI string) bool {
|
||||
var redirectURIs []string
|
||||
if err := json.Unmarshal([]byte(client.RedirectURIs), &redirectURIs); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to unmarshal redirect URIs")
|
||||
return false
|
||||
}
|
||||
|
||||
for _, uri := range redirectURIs {
|
||||
if uri == redirectURI {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) ValidateGrantType(client *model.OIDCClient, grantType string) bool {
|
||||
var grantTypes []string
|
||||
if err := json.Unmarshal([]byte(client.GrantTypes), &grantTypes); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to unmarshal grant types")
|
||||
return false
|
||||
}
|
||||
|
||||
for _, gt := range grantTypes {
|
||||
if gt == grantType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) ValidateResponseType(client *model.OIDCClient, responseType string) bool {
|
||||
var responseTypes []string
|
||||
if err := json.Unmarshal([]byte(client.ResponseTypes), &responseTypes); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to unmarshal response types")
|
||||
return false
|
||||
}
|
||||
|
||||
for _, rt := range responseTypes {
|
||||
if rt == responseType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) ValidateScope(client *model.OIDCClient, requestedScopes string) ([]string, error) {
|
||||
var allowedScopes []string
|
||||
if err := json.Unmarshal([]byte(client.Scopes), &allowedScopes); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal scopes: %w", err)
|
||||
}
|
||||
|
||||
requestedScopesList := []string{}
|
||||
if requestedScopes != "" {
|
||||
requestedScopesList = splitScopes(requestedScopes)
|
||||
}
|
||||
|
||||
validScopes := []string{}
|
||||
for _, scope := range requestedScopesList {
|
||||
for _, allowed := range allowedScopes {
|
||||
if scope == allowed {
|
||||
validScopes = append(validScopes, scope)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validScopes, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) GenerateAuthorizationCode(userContext *config.UserContext, clientID string, redirectURI string, scopes []string, nonce string, codeChallenge string, codeChallengeMethod string) (string, error) {
|
||||
code := uuid.New().String()
|
||||
now := time.Now()
|
||||
expiresAt := now.Add(10 * time.Minute).Unix()
|
||||
|
||||
// Store authorization code in database for replay protection
|
||||
authCodeRecord := model.OIDCAuthorizationCode{
|
||||
Code: code,
|
||||
ClientID: clientID,
|
||||
RedirectURI: redirectURI,
|
||||
Used: false,
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: now.Unix(),
|
||||
}
|
||||
|
||||
if err := oidc.config.Database.Create(&authCodeRecord).Error; err != nil {
|
||||
return "", fmt.Errorf("failed to store authorization code: %w", err)
|
||||
}
|
||||
|
||||
// Encode as JWT for stateless operation (but code is tracked in DB)
|
||||
claims := jwt.MapClaims{
|
||||
"code": code,
|
||||
"username": userContext.Username,
|
||||
"email": userContext.Email,
|
||||
"name": userContext.Name,
|
||||
"provider": userContext.Provider,
|
||||
"client_id": clientID,
|
||||
"redirect_uri": redirectURI,
|
||||
"scopes": scopes,
|
||||
"exp": expiresAt,
|
||||
"iat": now.Unix(),
|
||||
}
|
||||
|
||||
if nonce != "" {
|
||||
claims["nonce"] = nonce
|
||||
}
|
||||
|
||||
// Store PKCE challenge if provided
|
||||
if codeChallenge != "" {
|
||||
claims["code_challenge"] = codeChallenge
|
||||
if codeChallengeMethod != "" {
|
||||
claims["code_challenge_method"] = codeChallengeMethod
|
||||
} else {
|
||||
// Default to plain if method not specified
|
||||
claims["code_challenge_method"] = "plain"
|
||||
}
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
codeToken, err := token.SignedString(oidc.privateKey)
|
||||
if err != nil {
|
||||
// Clean up the database record if JWT signing fails
|
||||
oidc.config.Database.Delete(&authCodeRecord)
|
||||
return "", fmt.Errorf("failed to sign authorization code: %w", err)
|
||||
}
|
||||
|
||||
return codeToken, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) ValidateAuthorizationCode(codeToken string, clientID string, redirectURI string) (*config.UserContext, []string, string, string, string, error) {
|
||||
token, err := jwt.Parse(codeToken, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return oidc.publicKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, "", "", "", fmt.Errorf("failed to parse authorization code: %w", err)
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, nil, "", "", "", errors.New("invalid authorization code")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, nil, "", "", "", errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Extract code from JWT for database lookup
|
||||
code, ok := claims["code"].(string)
|
||||
if !ok || code == "" {
|
||||
return nil, nil, "", "", "", errors.New("missing code in authorization code token")
|
||||
}
|
||||
|
||||
// Check database for replay protection - verify code exists and hasn't been used
|
||||
var authCodeRecord model.OIDCAuthorizationCode
|
||||
err = oidc.config.Database.Where("code = ?", code).First(&authCodeRecord).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, "", "", "", errors.New("authorization code not found")
|
||||
}
|
||||
return nil, nil, "", "", "", fmt.Errorf("failed to query authorization code: %w", err)
|
||||
}
|
||||
|
||||
// Check if code has already been used (replay attack protection)
|
||||
if authCodeRecord.Used {
|
||||
return nil, nil, "", "", "", errors.New("authorization code has already been used")
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if time.Now().Unix() > authCodeRecord.ExpiresAt {
|
||||
return nil, nil, "", "", "", errors.New("authorization code expired")
|
||||
}
|
||||
|
||||
// Verify client_id and redirect_uri match
|
||||
if claims["client_id"] != clientID {
|
||||
return nil, nil, "", "", "", errors.New("client_id mismatch")
|
||||
}
|
||||
|
||||
if claims["redirect_uri"] != redirectURI {
|
||||
return nil, nil, "", "", "", errors.New("redirect_uri mismatch")
|
||||
}
|
||||
|
||||
// Verify database record matches request parameters
|
||||
if authCodeRecord.ClientID != clientID {
|
||||
return nil, nil, "", "", "", errors.New("client_id mismatch")
|
||||
}
|
||||
|
||||
if authCodeRecord.RedirectURI != redirectURI {
|
||||
return nil, nil, "", "", "", errors.New("redirect_uri mismatch")
|
||||
}
|
||||
|
||||
// Mark code as used to prevent replay attacks
|
||||
authCodeRecord.Used = true
|
||||
if err := oidc.config.Database.Save(&authCodeRecord).Error; err != nil {
|
||||
return nil, nil, "", "", "", fmt.Errorf("failed to mark authorization code as used: %w", err)
|
||||
}
|
||||
|
||||
userContext := &config.UserContext{
|
||||
Username: getStringClaim(claims, "username"),
|
||||
Email: getStringClaim(claims, "email"),
|
||||
Name: getStringClaim(claims, "name"),
|
||||
Provider: getStringClaim(claims, "provider"),
|
||||
IsLoggedIn: true,
|
||||
}
|
||||
|
||||
scopes := []string{}
|
||||
if scopesInterface, ok := claims["scopes"].([]interface{}); ok {
|
||||
for _, s := range scopesInterface {
|
||||
if scope, ok := s.(string); ok {
|
||||
scopes = append(scopes, scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonce := getStringClaim(claims, "nonce")
|
||||
codeChallenge := getStringClaim(claims, "code_challenge")
|
||||
codeChallengeMethod := getStringClaim(claims, "code_challenge_method")
|
||||
|
||||
return userContext, scopes, nonce, codeChallenge, codeChallengeMethod, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) ValidatePKCE(codeChallenge string, codeChallengeMethod string, codeVerifier string) error {
|
||||
if codeChallenge == "" {
|
||||
// PKCE not used, validation passes
|
||||
return nil
|
||||
}
|
||||
|
||||
if codeVerifier == "" {
|
||||
return errors.New("code_verifier required when code_challenge is present")
|
||||
}
|
||||
|
||||
switch codeChallengeMethod {
|
||||
case "S256":
|
||||
// Compute SHA256 hash of code_verifier
|
||||
hash := sha256.Sum256([]byte(codeVerifier))
|
||||
// Base64URL encode (without padding)
|
||||
computedChallenge := base64.RawURLEncoding.EncodeToString(hash[:])
|
||||
if computedChallenge != codeChallenge {
|
||||
return errors.New("code_verifier does not match code_challenge")
|
||||
}
|
||||
case "plain":
|
||||
// Direct comparison
|
||||
if codeVerifier != codeChallenge {
|
||||
return errors.New("code_verifier does not match code_challenge")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported code_challenge_method: %s", codeChallengeMethod)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) GenerateAccessToken(userContext *config.UserContext, clientID string, scopes []string) (string, error) {
|
||||
expiry := oidc.config.AccessTokenExpiry
|
||||
if expiry <= 0 {
|
||||
expiry = 3600 // Default 1 hour
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
claims := jwt.MapClaims{
|
||||
"sub": userContext.Username,
|
||||
"iss": oidc.config.Issuer,
|
||||
"aud": clientID,
|
||||
"exp": now.Add(time.Duration(expiry) * time.Second).Unix(),
|
||||
"iat": now.Unix(),
|
||||
"scope": joinScopes(scopes),
|
||||
"client_id": clientID,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
accessToken, err := token.SignedString(oidc.privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign access token: %w", err)
|
||||
}
|
||||
|
||||
return accessToken, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) ValidateAccessToken(accessToken string) (*config.UserContext, error) {
|
||||
return oidc.ValidateAccessTokenForClient(accessToken, "")
|
||||
}
|
||||
|
||||
// ValidateAccessTokenForClient validates an access token and optionally checks the audience claim.
|
||||
// If expectedClientID is provided, validates that the token's audience matches the expected client ID.
|
||||
// This prevents tokens issued for one client from being used by another client.
|
||||
func (oidc *OIDCService) ValidateAccessTokenForClient(accessToken string, expectedClientID string) (*config.UserContext, error) {
|
||||
token, err := jwt.Parse(accessToken, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return oidc.publicKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse access token: %w", err)
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid access token")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Verify issuer
|
||||
iss, ok := claims["iss"].(string)
|
||||
if !ok || iss != oidc.config.Issuer {
|
||||
return nil, errors.New("invalid issuer")
|
||||
}
|
||||
|
||||
// Verify audience if expected client ID is provided
|
||||
if expectedClientID != "" {
|
||||
aud, ok := claims["aud"].(string)
|
||||
if !ok || aud != expectedClientID {
|
||||
return nil, errors.New("invalid audience")
|
||||
}
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
exp, ok := claims["exp"].(float64)
|
||||
if !ok || time.Now().Unix() > int64(exp) {
|
||||
return nil, errors.New("access token expired")
|
||||
}
|
||||
|
||||
// Extract user info from claims
|
||||
username, ok := claims["sub"].(string)
|
||||
if !ok || username == "" {
|
||||
return nil, errors.New("missing sub claim")
|
||||
}
|
||||
|
||||
// Extract email and name if available
|
||||
email, _ := claims["email"].(string)
|
||||
name, _ := claims["name"].(string)
|
||||
|
||||
// Create user context
|
||||
userContext := &config.UserContext{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Name: name,
|
||||
IsLoggedIn: true,
|
||||
}
|
||||
|
||||
return userContext, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) GenerateIDToken(userContext *config.UserContext, clientID string, nonce string) (string, error) {
|
||||
expiry := oidc.config.IDTokenExpiry
|
||||
if expiry <= 0 {
|
||||
expiry = 3600 // Default 1 hour
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
claims := jwt.MapClaims{
|
||||
"sub": userContext.Username,
|
||||
"iss": oidc.config.Issuer,
|
||||
"aud": clientID,
|
||||
"exp": now.Add(time.Duration(expiry) * time.Second).Unix(),
|
||||
"iat": now.Unix(),
|
||||
"auth_time": now.Unix(),
|
||||
"email": userContext.Email,
|
||||
"name": userContext.Name,
|
||||
"preferred_username": userContext.Username,
|
||||
}
|
||||
|
||||
if nonce != "" {
|
||||
claims["nonce"] = nonce
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
idToken, err := token.SignedString(oidc.privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign ID token: %w", err)
|
||||
}
|
||||
|
||||
return idToken, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) GetJWKS() (map[string]interface{}, error) {
|
||||
// Extract modulus and exponent from public key
|
||||
n := oidc.publicKey.N
|
||||
e := oidc.publicKey.E
|
||||
|
||||
nBytes := n.Bytes()
|
||||
// Use minimal-octet encoding for exponent per RFC 7517
|
||||
eBytes := big.NewInt(int64(e)).Bytes()
|
||||
|
||||
jwk := map[string]interface{}{
|
||||
"kty": "RSA",
|
||||
"use": "sig",
|
||||
"kid": "default",
|
||||
"n": base64.RawURLEncoding.EncodeToString(nBytes),
|
||||
"e": base64.RawURLEncoding.EncodeToString(eBytes),
|
||||
"alg": "RS256",
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"keys": []interface{}{jwk},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) GetIssuer() string {
|
||||
return oidc.config.Issuer
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) GetAccessTokenExpiry() int {
|
||||
if oidc.config.AccessTokenExpiry <= 0 {
|
||||
return 3600 // Default 1 hour
|
||||
}
|
||||
return oidc.config.AccessTokenExpiry
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) SyncClientsFromConfig(clients map[string]config.OIDCClientConfig) error {
|
||||
for clientID, clientConfig := range clients {
|
||||
// Get client secret from config or file (similar to OAuth providers)
|
||||
clientSecret := utils.GetSecret(clientConfig.ClientSecret, clientConfig.ClientSecretFile)
|
||||
|
||||
if clientSecret == "" {
|
||||
log.Warn().Str("client_id", clientID).Msg("Client secret is empty, skipping client")
|
||||
continue
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
clientName := clientConfig.ClientName
|
||||
if clientName == "" {
|
||||
clientName = clientID
|
||||
}
|
||||
|
||||
redirectURIs := clientConfig.RedirectURIs
|
||||
if len(redirectURIs) == 0 {
|
||||
log.Warn().Str("client_id", clientID).Msg("No redirect URIs configured for client")
|
||||
continue
|
||||
}
|
||||
|
||||
grantTypes := clientConfig.GrantTypes
|
||||
if len(grantTypes) == 0 {
|
||||
grantTypes = []string{"authorization_code"}
|
||||
}
|
||||
|
||||
responseTypes := clientConfig.ResponseTypes
|
||||
if len(responseTypes) == 0 {
|
||||
responseTypes = []string{"code"}
|
||||
}
|
||||
|
||||
scopes := clientConfig.Scopes
|
||||
if len(scopes) == 0 {
|
||||
scopes = []string{"openid", "profile", "email"}
|
||||
}
|
||||
|
||||
// Serialize arrays to JSON
|
||||
redirectURIsJSON, err := json.Marshal(redirectURIs)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("client_id", clientID).Msg("Failed to marshal redirect URIs")
|
||||
continue
|
||||
}
|
||||
|
||||
grantTypesJSON, err := json.Marshal(grantTypes)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("client_id", clientID).Msg("Failed to marshal grant types")
|
||||
continue
|
||||
}
|
||||
|
||||
responseTypesJSON, err := json.Marshal(responseTypes)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("client_id", clientID).Msg("Failed to marshal response types")
|
||||
continue
|
||||
}
|
||||
|
||||
scopesJSON, err := json.Marshal(scopes)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("client_id", clientID).Msg("Failed to marshal scopes")
|
||||
continue
|
||||
}
|
||||
|
||||
// Hash client secret with bcrypt before storing
|
||||
hashedSecret, err := bcrypt.GenerateFromPassword([]byte(clientSecret), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("client_id", clientID).Msg("Failed to hash client secret")
|
||||
continue
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
// Check if client exists
|
||||
var existingClient model.OIDCClient
|
||||
err = oidc.config.Database.Where("client_id = ?", clientID).First(&existingClient).Error
|
||||
|
||||
client := model.OIDCClient{
|
||||
ClientID: clientID,
|
||||
ClientSecret: string(hashedSecret),
|
||||
ClientName: clientName,
|
||||
RedirectURIs: string(redirectURIsJSON),
|
||||
GrantTypes: string(grantTypesJSON),
|
||||
ResponseTypes: string(responseTypesJSON),
|
||||
Scopes: string(scopesJSON),
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Create new client
|
||||
client.CreatedAt = now
|
||||
if err := oidc.config.Database.Create(&client).Error; err != nil {
|
||||
log.Error().Err(err).Str("client_id", clientID).Msg("Failed to create OIDC client")
|
||||
continue
|
||||
}
|
||||
log.Info().Str("client_id", clientID).Str("client_name", clientName).Msg("Created OIDC client from config")
|
||||
} else if err == nil {
|
||||
// Update existing client
|
||||
client.CreatedAt = existingClient.CreatedAt // Preserve original creation time
|
||||
if err := oidc.config.Database.Where("client_id = ?", clientID).Updates(&client).Error; err != nil {
|
||||
log.Error().Err(err).Str("client_id", clientID).Msg("Failed to update OIDC client")
|
||||
continue
|
||||
}
|
||||
log.Info().Str("client_id", clientID).Str("client_name", clientName).Msg("Updated OIDC client from config")
|
||||
} else {
|
||||
log.Error().Err(err).Str("client_id", clientID).Msg("Failed to check existing OIDC client")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func splitScopes(scopes string) []string {
|
||||
if scopes == "" {
|
||||
return []string{}
|
||||
}
|
||||
parts := strings.Split(scopes, " ")
|
||||
result := []string{}
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func joinScopes(scopes []string) string {
|
||||
return strings.Join(scopes, " ")
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getStringClaim(claims jwt.MapClaims, key string) string {
|
||||
if val, ok := claims[key].(string); ok {
|
||||
return val
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -2,12 +2,10 @@ package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/weppos/publicsuffix-go/publicsuffix"
|
||||
@@ -23,13 +21,13 @@ func GetCookieDomain(u string) (string, error) {
|
||||
host := parsed.Hostname()
|
||||
|
||||
if netIP := net.ParseIP(host); netIP != nil {
|
||||
return "", fmt.Errorf("IP addresses not allowed for app url '%s' (got IP: %s)", u, host)
|
||||
return "", errors.New("IP addresses not allowed")
|
||||
}
|
||||
|
||||
parts := strings.Split(host, ".")
|
||||
|
||||
if len(parts) < 3 {
|
||||
return "", fmt.Errorf("invalid app url '%s', must be at least second level domain (got %d parts, need 3+)", u, len(parts))
|
||||
return "", errors.New("invalid app url, must be at least second level domain")
|
||||
}
|
||||
|
||||
domain := strings.Join(parts[1:], ".")
|
||||
@@ -37,7 +35,7 @@ func GetCookieDomain(u string) (string, error) {
|
||||
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, domain, nil)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("domain '%s' (from app url '%s') is in public suffix list, cannot set cookies", domain, u)
|
||||
return "", errors.New("domain in public suffix list, cannot set cookies")
|
||||
}
|
||||
|
||||
return domain, nil
|
||||
|
||||
@@ -2,9 +2,8 @@ package utils_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
@@ -2,9 +2,8 @@ package decoders_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/decoders"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/utils/decoders"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -2,8 +2,7 @@ package utils_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -3,8 +3,7 @@ package loaders
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"tinyauth/internal/config"
|
||||
|
||||
"github.com/traefik/paerser/cli"
|
||||
"github.com/traefik/paerser/env"
|
||||
|
||||
@@ -16,25 +16,18 @@ func (f *FileLoader) Load(args []string, cmd *cli.Command) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check for experimental config file flag (supports both traefik.* and direct format)
|
||||
// Note: paerser converts flags to lowercase, so we check lowercase versions
|
||||
configFilePath := ""
|
||||
if val, ok := flags["traefik.experimental.configfile"]; ok {
|
||||
configFilePath = val
|
||||
} else if val, ok := flags["experimental.configfile"]; ok {
|
||||
configFilePath = val
|
||||
}
|
||||
// I guess we are using traefik as the root name
|
||||
configFileFlag := "traefik.experimental.configFile"
|
||||
|
||||
if configFilePath == "" {
|
||||
if _, ok := flags[configFileFlag]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
log.Warn().Str("configFile", configFilePath).Msg("Using experimental file config loader, this feature is experimental and may change or be removed in future releases")
|
||||
log.Warn().Msg("Using experimental file config loader, this feature is experimental and may change or be removed in future releases")
|
||||
|
||||
err = file.Decode(configFilePath, cmd.Configuration)
|
||||
err = file.Decode(flags[configFileFlag], cmd.Configuration)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("configFile", configFilePath).Msg("Failed to decode config file")
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ package utils_test
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -2,8 +2,7 @@ package utils_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -3,8 +3,7 @@ package utils
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"tinyauth/internal/config"
|
||||
)
|
||||
|
||||
func ParseUsers(users string) ([]config.User, error) {
|
||||
|
||||
@@ -3,8 +3,7 @@ package utils_test
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install --no-cache-dir requests authlib
|
||||
|
||||
COPY oidc_whoami.py /app/oidc_whoami.py
|
||||
|
||||
RUN chmod +x /app/oidc_whoami.py
|
||||
|
||||
EXPOSE 8765
|
||||
|
||||
CMD ["python3", "/app/oidc_whoami.py"]
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
# OIDC Validation Setup
|
||||
|
||||
This directory contains a docker-compose setup for testing tinyauth's OIDC provider functionality with a minimal test client.
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Build the OIDC test client image:**
|
||||
```bash
|
||||
docker build -t oidc-whoami-test:latest .
|
||||
```
|
||||
|
||||
2. **Start the services:**
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
### nginx
|
||||
- **Purpose:** Reverse proxy for `auth.example.com` → tinyauth
|
||||
- **Ports:** 80 (exposed to host)
|
||||
- **Access:** http://auth.example.com/ (via nginx on port 80)
|
||||
|
||||
### dns
|
||||
- **Purpose:** DNS server (dnsmasq) that resolves `auth.example.com` to the tinyauth container
|
||||
- **Configuration:** Resolves `auth.example.com` to the `tinyauth` container IP (172.28.0.20) within the Docker network
|
||||
- **Ports:** 53 (UDP/TCP) - not exposed to host (only for container-to-container communication)
|
||||
|
||||
### tinyauth
|
||||
- **URL:** http://auth.example.com/ (via nginx)
|
||||
- **Credentials:** `user` / `pass`
|
||||
- **OIDC Discovery:** http://auth.example.com/api/.well-known/openid-configuration
|
||||
- **OIDC Client ID:** `testclient`
|
||||
- **OIDC Client Secret:** `test-secret-123`
|
||||
- **Ports:** Not exposed to host (accessed via nginx on port 80)
|
||||
|
||||
### oidc-whoami
|
||||
- **Callback URL:** http://localhost:8765/callback
|
||||
- **Purpose:** Minimal OIDC test client that validates the OIDC flow
|
||||
- **Ports:** 8765 (exposed to host)
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Start all services:**
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
2. **Launch Chrome with host-resolver-rules:**
|
||||
```bash
|
||||
./launch-chrome-host.sh
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```bash
|
||||
google-chrome \
|
||||
--host-resolver-rules="MAP auth.example.com 127.0.0.1" \
|
||||
--disable-features=HttpsOnlyMode \
|
||||
--unsafely-treat-insecure-origin-as-secure=http://auth.example.com \
|
||||
--user-data-dir=/tmp/chrome-test-profile \
|
||||
http://auth.example.com/
|
||||
```
|
||||
|
||||
**Note:** The `--user-data-dir` flag uses a temporary profile to avoid HSTS (HTTP Strict Transport Security) issues that might force HTTPS redirects.
|
||||
|
||||
3. **Access tinyauth:** http://auth.example.com/
|
||||
- Login with: `user` / `pass`
|
||||
|
||||
4. **Test OIDC flow:**
|
||||
```bash
|
||||
# Get authorization URL from oidc-whoami logs
|
||||
docker compose logs oidc-whoami | grep "Authorization URL"
|
||||
# Open that URL in Chrome (already configured with host-resolver-rules)
|
||||
```
|
||||
|
||||
## Connecting from Chrome/Browser
|
||||
|
||||
Since the DNS server is only accessible within the Docker network, you have several options to access `auth.example.com` from your browser:
|
||||
|
||||
### Option 1: Use /etc/hosts (Simplest)
|
||||
|
||||
Add this line to your `/etc/hosts` file (or `C:\Windows\System32\drivers\etc\hosts` on Windows):
|
||||
|
||||
```
|
||||
127.0.0.1 auth.example.com
|
||||
```
|
||||
|
||||
Then access: http://auth.example.com/
|
||||
|
||||
**To edit /etc/hosts on Linux/Mac:**
|
||||
```bash
|
||||
sudo nano /etc/hosts
|
||||
# Add: 127.0.0.1 auth.example.com
|
||||
```
|
||||
|
||||
**To edit hosts on Windows:**
|
||||
1. Open Notepad as Administrator
|
||||
2. Open `C:\Windows\System32\drivers\etc\hosts`
|
||||
3. Add: `127.0.0.1 auth.example.com`
|
||||
|
||||
### Option 2: Use Chrome's `--host-resolver-rules` (Chrome-specific, No System Changes)
|
||||
|
||||
Chrome has a command-line flag that lets you map hostnames directly, bypassing DNS entirely. This is perfect for testing without modifying system settings.
|
||||
|
||||
**To use it:**
|
||||
|
||||
1. **Make sure services are running:**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
2. **Launch Chrome with the host resolver rule:**
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
google-chrome --host-resolver-rules="MAP auth.example.com 127.0.0.1"
|
||||
```
|
||||
|
||||
**Mac:**
|
||||
```bash
|
||||
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
||||
--host-resolver-rules="MAP auth.example.com 127.0.0.1"
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
"C:\Program Files\Google\ Chrome\Application\chrome.exe" --host-resolver-rules="MAP auth.example.com 127.0.0.1"
|
||||
```
|
||||
|
||||
3. **Or modify Chrome's shortcut:**
|
||||
- Right-click Chrome shortcut → Properties
|
||||
- In "Target" field, append: ` --host-resolver-rules="MAP auth.example.com 127.0.0.1"`
|
||||
- Click OK
|
||||
|
||||
4. **Access:** http://auth.example.com/
|
||||
|
||||
**Note:** This only affects Chrome, not other applications. The DNS server on port 5353 isn't needed for this approach.
|
||||
|
||||
### Option 3: Use System DNS (All Applications)
|
||||
|
||||
If you want to use the DNS server on port 5353 for all applications (not just Chrome), configure your system DNS:
|
||||
|
||||
**Linux (with systemd-resolved):**
|
||||
```bash
|
||||
# Configure systemd-resolved to use our DNS
|
||||
sudo resolvectl dns lo 127.0.0.1:5353
|
||||
```
|
||||
|
||||
**Linux (without systemd-resolved):**
|
||||
```bash
|
||||
# Edit /etc/resolv.conf
|
||||
sudo nano /etc/resolv.conf
|
||||
# Add: nameserver 127.0.0.1
|
||||
# Note: This won't work with port 5353, you'd need port 53
|
||||
```
|
||||
|
||||
**Note:** Most systems expect DNS on port 53. To use port 5353, you'd need a DNS proxy or configure Chrome specifically (see Option 2 above).
|
||||
|
||||
## Testing
|
||||
|
||||
1. Start the services with `docker compose up --build -d`
|
||||
2. Launch Chrome: `./launch-chrome-host.sh` (or use `--host-resolver-rules` manually)
|
||||
3. Navigate to: http://auth.example.com/
|
||||
4. Login with `user` / `pass`
|
||||
5. Test the OIDC flow by accessing the discovery endpoint: http://auth.example.com/api/.well-known/openid-configuration
|
||||
|
||||
## Configuration
|
||||
|
||||
The tinyauth configuration is in `config.yaml`:
|
||||
- OIDC is enabled
|
||||
- Single user: `user` with password `pass`
|
||||
- OIDC client `testclient` is configured with redirect URI `http://localhost:8765/callback`
|
||||
- App URL and OIDC issuer: `http://auth.example.com` (via nginx on port 80)
|
||||
|
||||
## Notes
|
||||
|
||||
- All containers are on a custom Docker network (`tinyauth-network`) with a DNS server for domain resolution
|
||||
- The DNS server resolves `auth.example.com` to the tinyauth container within the network
|
||||
- The redirect URI must match exactly what's configured in tinyauth
|
||||
- Data is persisted in the `./data` directory
|
||||
- The domain `auth.example.com` is used to satisfy cookie domain validation requirements (needs at least 3 domain parts and not in public suffix list)
|
||||
@@ -1,36 +0,0 @@
|
||||
appUrl: "http://auth.example.com"
|
||||
logLevel: "info"
|
||||
databasePath: "/data/tinyauth.db"
|
||||
|
||||
auth:
|
||||
users: "user:$2b$12$mWEdxub8KTTBLK/f7dloKOS4t3kIeLOpme5pMXci5.lXNPANjCT5u" # user:pass
|
||||
secureCookie: false
|
||||
sessionExpiry: 3600
|
||||
loginTimeout: 300
|
||||
loginMaxRetries: 3
|
||||
|
||||
oidc:
|
||||
enabled: true
|
||||
issuer: "http://auth.example.com"
|
||||
accessTokenExpiry: 3600
|
||||
idTokenExpiry: 3600
|
||||
clients:
|
||||
testclient:
|
||||
clientSecret: "test-secret-123"
|
||||
clientName: "OIDC Test Client"
|
||||
redirectUris:
|
||||
- "http://client.example.com/callback"
|
||||
- "http://localhost:8765/callback"
|
||||
- "http://127.0.0.1:8765/callback"
|
||||
grantTypes:
|
||||
- "authorization_code"
|
||||
responseTypes:
|
||||
- "code"
|
||||
scopes:
|
||||
- "openid"
|
||||
- "profile"
|
||||
- "email"
|
||||
|
||||
ui:
|
||||
title: "Tinyauth OIDC Test"
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
dns:
|
||||
container_name: dns-server
|
||||
image: strm/dnsmasq:latest
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
command:
|
||||
- "--no-daemon"
|
||||
- "--log-queries"
|
||||
- "--no-resolv"
|
||||
- "--server=8.8.8.8"
|
||||
- "--server=8.8.4.4"
|
||||
- "--address=/auth.example.com/172.28.0.2"
|
||||
- "--address=/client.example.com/172.28.0.2"
|
||||
# DNS port not exposed to host - only needed for container-to-container communication
|
||||
# Chrome uses --host-resolver-rules instead
|
||||
networks:
|
||||
tinyauth-network:
|
||||
ipv4_address: 172.28.0.10
|
||||
|
||||
nginx:
|
||||
container_name: nginx-proxy
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
networks:
|
||||
- tinyauth-network
|
||||
# Use Docker's built-in DNS (127.0.0.11) for service name resolution
|
||||
# Our custom DNS (172.28.0.10) is only used via resolver directive in nginx.conf
|
||||
depends_on:
|
||||
- tinyauth
|
||||
- dns
|
||||
- oidc-whoami
|
||||
|
||||
|
||||
tinyauth:
|
||||
container_name: tinyauth-oidc-test
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
command: ["--experimental.configfile=/config/config.yaml"]
|
||||
# Port not exposed to host - accessed via nginx
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./config.yaml:/config/config.yaml:ro
|
||||
networks:
|
||||
tinyauth-network:
|
||||
ipv4_address: 172.28.0.20
|
||||
depends_on:
|
||||
- dns
|
||||
healthcheck:
|
||||
test: ["CMD", "tinyauth", "healthcheck"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
oidc-whoami:
|
||||
container_name: oidc-whoami-test
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- OIDC_ISSUER=http://auth.example.com
|
||||
- CLIENT_ID=testclient
|
||||
- CLIENT_SECRET=test-secret-123
|
||||
# Port not exposed to host - accessed via nginx
|
||||
depends_on:
|
||||
- tinyauth
|
||||
- dns
|
||||
# Use Docker's built-in DNS first, then our custom DNS for custom domains
|
||||
dns:
|
||||
- 127.0.0.11
|
||||
- 172.28.0.10
|
||||
networks:
|
||||
tinyauth-network:
|
||||
ipv4_address: 172.28.0.30
|
||||
# Note: Using custom network with DNS server to resolve auth.example.test
|
||||
# The redirect URI must match what's configured in tinyauth (http://localhost:8765/callback)
|
||||
# Using auth.example.test domain to satisfy cookie domain validation requirements (needs 3+ parts, not in public suffix list)
|
||||
|
||||
networks:
|
||||
tinyauth-network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.28.0.0/16
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Launch Chrome from host (not in container)
|
||||
# This script should be run on your host machine
|
||||
|
||||
set -e
|
||||
|
||||
echo "Launching Chrome for OIDC test setup..."
|
||||
|
||||
# Detect Chrome
|
||||
if command -v google-chrome &> /dev/null; then
|
||||
CHROME_CMD="google-chrome"
|
||||
elif command -v chromium-browser &> /dev/null; then
|
||||
CHROME_CMD="chromium-browser"
|
||||
elif command -v chromium &> /dev/null; then
|
||||
CHROME_CMD="chromium"
|
||||
elif [ -f "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ]; then
|
||||
CHROME_CMD="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
else
|
||||
echo "Error: Chrome not found. Please install Google Chrome or Chromium."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Using: $CHROME_CMD"
|
||||
echo "Opening: http://client.example.com/ (OIDC test client)"
|
||||
echo ""
|
||||
|
||||
$CHROME_CMD \
|
||||
--host-resolver-rules="MAP auth.example.com 127.0.0.1, MAP client.example.com 127.0.0.1" \
|
||||
--disable-features=HttpsOnlyMode \
|
||||
--unsafely-treat-insecure-origin-as-secure=http://auth.example.com,http://client.example.com \
|
||||
--user-data-dir=/tmp/chrome-test-profile-$(date +%s) \
|
||||
--new-window \
|
||||
http://client.example.com/ \
|
||||
> /dev/null 2>&1 &
|
||||
|
||||
echo "Chrome launched!"
|
||||
echo "OIDC test client: http://client.example.com/"
|
||||
echo "Tinyauth: http://auth.example.com/"
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "Chrome Launcher for OIDC Test Setup"
|
||||
echo "=========================================="
|
||||
|
||||
# Wait for nginx to be ready
|
||||
echo "Waiting for nginx to be ready..."
|
||||
for i in {1..30}; do
|
||||
if curl -s http://127.0.0.1/ > /dev/null 2>&1; then
|
||||
echo "✓ Nginx is ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo "✗ Nginx not ready after 30 seconds"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Try to find Chrome on the host system
|
||||
# Since we're in a container, we need to check common locations
|
||||
CHROME_PATHS=(
|
||||
"/usr/bin/google-chrome"
|
||||
"/usr/bin/google-chrome-stable"
|
||||
"/usr/bin/chromium-browser"
|
||||
"/usr/bin/chromium"
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
)
|
||||
|
||||
CHROME_CMD=""
|
||||
for path in "${CHROME_PATHS[@]}"; do
|
||||
if [ -f "$path" ] || command -v "$(basename "$path")" &> /dev/null; then
|
||||
CHROME_CMD="$(basename "$path")"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$CHROME_CMD" ]; then
|
||||
echo ""
|
||||
echo "Chrome not found in container. This is expected."
|
||||
echo "Please launch Chrome manually on your host with:"
|
||||
echo ""
|
||||
echo ' google-chrome --host-resolver-rules="MAP auth.example.com 127.0.0.1" http://auth.example.com/'
|
||||
echo ""
|
||||
echo "Or use the launch script on your host:"
|
||||
echo " ./launch-chrome.sh"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found Chrome: $CHROME_CMD"
|
||||
echo "Launching Chrome with host-resolver-rules..."
|
||||
echo ""
|
||||
|
||||
$CHROME_CMD \
|
||||
--host-resolver-rules="MAP auth.example.com 127.0.0.1" \
|
||||
--new-window \
|
||||
http://auth.example.com/ \
|
||||
> /dev/null 2>&1 &
|
||||
|
||||
echo "✓ Chrome launched!"
|
||||
echo ""
|
||||
echo "Access tinyauth at: http://auth.example.com/"
|
||||
echo "OIDC test client callback: http://127.0.0.1:8765/callback"
|
||||
echo ""
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
# Use Docker's built-in DNS (127.0.0.11) for service name resolution
|
||||
# This allows nginx to resolve Docker service names like "tinyauth" and "oidc-whoami"
|
||||
resolver 127.0.0.11 valid=10s;
|
||||
resolver_timeout 5s;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name auth.example.com;
|
||||
|
||||
location / {
|
||||
# Use variable to enable dynamic resolution at request time
|
||||
set $backend "tinyauth:3000";
|
||||
proxy_pass http://$backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name client.example.com;
|
||||
|
||||
location / {
|
||||
# Use variable to enable dynamic resolution at request time
|
||||
set $backend "oidc-whoami:8765";
|
||||
proxy_pass http://$backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import html
|
||||
import webbrowser
|
||||
import secrets
|
||||
import time
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
import requests
|
||||
from authlib.integrations.requests_client import OAuth2Session
|
||||
from authlib.oidc.core import CodeIDToken
|
||||
from authlib.jose import jwt
|
||||
|
||||
# ---- config via env ----
|
||||
ISSUER = os.environ["OIDC_ISSUER"]
|
||||
CLIENT_ID = os.environ["CLIENT_ID"]
|
||||
CLIENT_SECRET= os.environ.get("CLIENT_SECRET") # optional (public clients ok)
|
||||
REDIRECT_URI = "http://client.example.com/callback"
|
||||
SCOPE = "openid profile email"
|
||||
|
||||
# ---- discovery ----
|
||||
# Retry discovery in case nginx isn't ready yet
|
||||
discovery = None
|
||||
for attempt in range(10):
|
||||
try:
|
||||
discovery = requests.get(
|
||||
f"{ISSUER.rstrip('/')}/api/.well-known/openid-configuration",
|
||||
timeout=5
|
||||
).json()
|
||||
break
|
||||
except Exception as e:
|
||||
if attempt < 9:
|
||||
print(f"Discovery attempt {attempt + 1} failed: {e}, retrying...")
|
||||
time.sleep(2)
|
||||
else:
|
||||
raise
|
||||
|
||||
if discovery is None:
|
||||
raise RuntimeError("Failed to fetch OIDC discovery document after 10 attempts")
|
||||
|
||||
state = secrets.token_urlsafe(16)
|
||||
nonce = secrets.token_urlsafe(16)
|
||||
|
||||
client = OAuth2Session(
|
||||
client_id=CLIENT_ID,
|
||||
client_secret=CLIENT_SECRET,
|
||||
scope=SCOPE,
|
||||
redirect_uri=REDIRECT_URI,
|
||||
)
|
||||
|
||||
auth_result = client.create_authorization_url(
|
||||
discovery["authorization_endpoint"],
|
||||
state=state,
|
||||
nonce=nonce,
|
||||
code_challenge_method="S256",
|
||||
)
|
||||
auth_url = auth_result[0]
|
||||
code_verifier = auth_result[1] if len(auth_result) > 1 else None
|
||||
|
||||
# Cache JWKS for token validation
|
||||
jwk_set_cache = None
|
||||
jwk_set_cache_time = None
|
||||
|
||||
def get_jwk_set():
|
||||
"""Get JWKS with caching"""
|
||||
global jwk_set_cache, jwk_set_cache_time
|
||||
# Cache for 1 hour
|
||||
if jwk_set_cache is None or (jwk_set_cache_time and time.time() - jwk_set_cache_time > 3600):
|
||||
jwk_set_cache = requests.get(discovery["jwks_uri"]).json()
|
||||
jwk_set_cache_time = time.time()
|
||||
return jwk_set_cache
|
||||
|
||||
def parse_cookies(cookie_header):
|
||||
"""Parse cookies from Cookie header"""
|
||||
if not cookie_header:
|
||||
return {}
|
||||
cookie = SimpleCookie()
|
||||
cookie.load(cookie_header)
|
||||
return {k: v.value for k, v in cookie.items()}
|
||||
|
||||
def validate_id_token(id_token):
|
||||
"""Validate and decode ID token"""
|
||||
try:
|
||||
jwk_set = get_jwk_set()
|
||||
claims_options = {
|
||||
"iss": {"essential": True, "value": discovery["issuer"]},
|
||||
"aud": {"essential": True, "value": CLIENT_ID},
|
||||
}
|
||||
decoded = jwt.decode(
|
||||
id_token,
|
||||
key=jwk_set,
|
||||
claims_options=claims_options
|
||||
)
|
||||
decoded.validate()
|
||||
return dict(decoded)
|
||||
except Exception as e:
|
||||
print(f"Token validation failed: {e}")
|
||||
return None
|
||||
|
||||
# ---- tiny callback server ----
|
||||
class CallbackHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
# Handle root path - check if already logged in
|
||||
if self.path == "/" or self.path == "":
|
||||
cookies = parse_cookies(self.headers.get("Cookie"))
|
||||
id_token = cookies.get("id_token")
|
||||
|
||||
# Check if we have a valid token
|
||||
if id_token:
|
||||
claims = validate_id_token(id_token)
|
||||
if claims and claims.get("exp", 0) > time.time():
|
||||
# Already logged in - show main page
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OIDC Test Client - Welcome</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}}
|
||||
.main-box {{
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}}
|
||||
h1 {{
|
||||
color: #4285f4;
|
||||
margin-top: 0;
|
||||
}}
|
||||
.user-info {{
|
||||
background: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #4285f4;
|
||||
}}
|
||||
pre {{
|
||||
background: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #ddd;
|
||||
}}
|
||||
.logout-btn {{
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-box">
|
||||
<h1>✅ Welcome back!</h1>
|
||||
<div class="user-info">
|
||||
<h2>User Information</h2>
|
||||
<p><strong>Username:</strong> {html.escape(str(claims.get('preferred_username', claims.get('sub', 'N/A'))))}</p>
|
||||
<p><strong>Name:</strong> {html.escape(str(claims.get('name', 'N/A')))}</p>
|
||||
<p><strong>Email:</strong> {html.escape(str(claims.get('email', 'N/A')))}</p>
|
||||
</div>
|
||||
<hr>
|
||||
<h2>ID Token Claims:</h2>
|
||||
<pre>{html.escape(json.dumps(claims, indent=2))}</pre>
|
||||
<a href="/logout" class="logout-btn">Logout</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.wfile.write(html_content.encode())
|
||||
return
|
||||
|
||||
# Not logged in - show login page
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>OIDC Test Client</title></head>
|
||||
<body>
|
||||
<h1>OIDC Test Client</h1>
|
||||
<p>Click the button below to start the OIDC flow:</p>
|
||||
<a href="{auth_url}" style="display: inline-block; padding: 10px 20px; background: #4285f4; color: white; text-decoration: none; border-radius: 4px;">Login with OIDC</a>
|
||||
<hr>
|
||||
<p><small>Authorization URL: <code>{auth_url}</code></small></p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.wfile.write(html_content.encode())
|
||||
return
|
||||
|
||||
# Handle logout
|
||||
if self.path == "/logout":
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/")
|
||||
self.send_header("Set-Cookie", "id_token=; Path=/; Max-Age=0")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
# Handle callback
|
||||
if not self.path.startswith("/callback"):
|
||||
self.send_error(404, "Not Found")
|
||||
return
|
||||
|
||||
qs = parse_qs(urlparse(self.path).query)
|
||||
|
||||
if qs.get("state", [None])[0] != state:
|
||||
self.send_error(400, "Invalid state")
|
||||
return
|
||||
|
||||
code = qs.get("code", [None])[0]
|
||||
if not code:
|
||||
self.send_error(400, "Missing code")
|
||||
return
|
||||
|
||||
token = client.fetch_token(
|
||||
discovery["token_endpoint"],
|
||||
code=code,
|
||||
code_verifier=code_verifier,
|
||||
)
|
||||
|
||||
# ---- ID token validation ----
|
||||
# Decode and validate the ID token using cached JWKS
|
||||
jwk_set = get_jwk_set()
|
||||
|
||||
# Decode the JWT - make nonce optional if not provided
|
||||
claims_options = {
|
||||
"iss": {"essential": True, "value": discovery["issuer"]},
|
||||
"aud": {"essential": True, "value": CLIENT_ID},
|
||||
}
|
||||
if nonce:
|
||||
claims_options["nonce"] = {"essential": True, "value": nonce}
|
||||
|
||||
decoded = jwt.decode(
|
||||
token["id_token"],
|
||||
key=jwk_set,
|
||||
claims_options=claims_options
|
||||
)
|
||||
decoded.validate()
|
||||
|
||||
# Convert JWTClaims to dict for display
|
||||
id_token_claims = dict(decoded)
|
||||
|
||||
# Store ID token in cookie (expires when token expires)
|
||||
token_expiry = id_token_claims.get("exp", 0) - time.time()
|
||||
max_age = max(0, int(token_expiry))
|
||||
|
||||
# Redirect to main page with cookie set
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/")
|
||||
self.send_header("Set-Cookie", f"id_token={token['id_token']}; Path=/; Max-Age={max_age}; HttpOnly")
|
||||
self.end_headers()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ OIDC Authentication Successful!")
|
||||
print("=" * 60)
|
||||
print("\nID Token Claims:")
|
||||
print(json.dumps(id_token_claims, indent=2))
|
||||
print("\n" + "=" * 60)
|
||||
# Don't exit - keep server running for multiple test flows
|
||||
|
||||
# ---- run ----
|
||||
print("=" * 60)
|
||||
print("OIDC Test Client")
|
||||
print("=" * 60)
|
||||
print(f"\nAuthorization URL: {auth_url}")
|
||||
print("\nTo test the OIDC flow:")
|
||||
print("1. Open the authorization URL above in your browser")
|
||||
print("2. Login with credentials: user / pass")
|
||||
print("3. You will be redirected back to the callback")
|
||||
print("4. The ID token claims will be displayed below")
|
||||
print(f"\nWaiting for callback on {REDIRECT_URI}...")
|
||||
print("=" * 60)
|
||||
|
||||
# Try to open browser (may fail in Docker, that's OK)
|
||||
try:
|
||||
webbrowser.open(auth_url)
|
||||
except Exception as e:
|
||||
print(f"Could not open browser automatically: {e}")
|
||||
print("Please open the authorization URL manually")
|
||||
|
||||
HTTPServer(("0.0.0.0", 8765), CallbackHandler).serve_forever()
|
||||
Reference in New Issue
Block a user