mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-02-22 00:42:03 +00:00
Compare commits
4 Commits
v5.0.0-alp
...
feat/gen-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b91db19d59 | ||
|
|
3f3072254e | ||
|
|
6112f977ea | ||
|
|
e078e8a3f0 |
245
.env.example
245
.env.example
@@ -1,99 +1,172 @@
|
||||
# Base Configuration
|
||||
# Tinyauth example configuration
|
||||
|
||||
# The base URL where Tinyauth is accessible
|
||||
TINYAUTH_APPURL="https://auth.example.com"
|
||||
# Directory for static resources
|
||||
TINYAUTH_RESOURCESDIR="/data/resources"
|
||||
# Path to SQLite database file
|
||||
TINYAUTH_DATABASEPATH="/data/tinyauth.db"
|
||||
# Disable version heartbeat
|
||||
TINYAUTH_DISABLEANALYTICS="false"
|
||||
# Disable static resource serving
|
||||
TINYAUTH_DISABLERESOURCES="false"
|
||||
# Disable UI warning messages
|
||||
TINYAUTH_DISABLEUIWARNINGS="false"
|
||||
# The base URL where the app is hosted.
|
||||
TINYAUTH_APPURL=
|
||||
# The directory where resources are stored.
|
||||
TINYAUTH_RESOURCESDIR="./resources"
|
||||
# The path to the database file.
|
||||
TINYAUTH_DATABASEPATH="./tinyauth.db"
|
||||
# Disable analytics.
|
||||
TINYAUTH_DISABLEANALYTICS=false
|
||||
# Disable resources server.
|
||||
TINYAUTH_DISABLERESOURCES=false
|
||||
|
||||
# Logging Configuration
|
||||
# server config
|
||||
|
||||
# Log level: trace, debug, info, warn, error
|
||||
TINYAUTH_LOG_LEVEL="info"
|
||||
# Enable JSON formatted logs
|
||||
TINYAUTH_LOG_JSON="false"
|
||||
# Specific Log stream configurations
|
||||
# APP and HTTP log streams are enabled by default, and use the global log level unless overridden
|
||||
TINYAUTH_LOG_STREAMS_APP_ENABLED="true"
|
||||
TINYAUTH_LOG_STREAMS_APP_LEVEL="info"
|
||||
TINYAUTH_LOG_STREAMS_HTTP_ENABLED="true"
|
||||
TINYAUTH_LOG_STREAMS_HTTP_LEVEL="info"
|
||||
TINYAUTH_LOG_STREAMS_AUDIT_ENABLED="false"
|
||||
TINYAUTH_LOG_STREAMS_AUDIT_LEVEL="info"
|
||||
|
||||
# Server Configuration
|
||||
|
||||
# Port to listen on
|
||||
TINYAUTH_SERVER_PORT="3000"
|
||||
# Interface to bind to (0.0.0.0 for all interfaces)
|
||||
# The port on which the server listens.
|
||||
TINYAUTH_SERVER_PORT=3000
|
||||
# The address on which the server listens.
|
||||
TINYAUTH_SERVER_ADDRESS="0.0.0.0"
|
||||
# Unix socket path (optional, overrides port/address if set)
|
||||
TINYAUTH_SERVER_SOCKETPATH=""
|
||||
# Comma-separated list of trusted proxy IPs/CIDRs
|
||||
TINYAUTH_SERVER_TRUSTEDPROXIES=""
|
||||
# The path to the Unix socket.
|
||||
TINYAUTH_SERVER_SOCKETPATH=
|
||||
|
||||
# Authentication Configuration
|
||||
# auth config
|
||||
|
||||
# Format: username:bcrypt_hash (use bcrypt to generate hash)
|
||||
TINYAUTH_AUTH_USERS="admin:$2a$10$example_bcrypt_hash_here"
|
||||
# Path to external users file (optional)
|
||||
TINYAUTH_AUTH_USERSFILE=""
|
||||
# Enable secure cookies (requires HTTPS)
|
||||
TINYAUTH_AUTH_SECURECOOKIE="true"
|
||||
# Session expiry in seconds (7200 = 2 hours)
|
||||
TINYAUTH_AUTH_SESSIONEXPIRY="7200"
|
||||
# Session maximum lifetime in seconds (0 = unlimited)
|
||||
TINYAUTH_AUTH_SESSIONMAXLIFETIME="0"
|
||||
# Login timeout in seconds (300 = 5 minutes)
|
||||
TINYAUTH_AUTH_LOGINTIMEOUT="300"
|
||||
# Maximum login retries before lockout
|
||||
TINYAUTH_AUTH_LOGINMAXRETRIES="5"
|
||||
# List of allowed IPs or CIDR ranges.
|
||||
TINYAUTH_AUTH_IP_ALLOW=
|
||||
# List of blocked IPs or CIDR ranges.
|
||||
TINYAUTH_AUTH_IP_BLOCK=
|
||||
# Comma-separated list of users (username:hashed_password).
|
||||
TINYAUTH_AUTH_USERS=
|
||||
# Path to the users file.
|
||||
TINYAUTH_AUTH_USERSFILE=
|
||||
# Enable secure cookies.
|
||||
TINYAUTH_AUTH_SECURECOOKIE=false
|
||||
# Session expiry time in seconds.
|
||||
TINYAUTH_AUTH_SESSIONEXPIRY=86400
|
||||
# Maximum session lifetime in seconds.
|
||||
TINYAUTH_AUTH_SESSIONMAXLIFETIME=0
|
||||
# Login timeout in seconds.
|
||||
TINYAUTH_AUTH_LOGINTIMEOUT=300
|
||||
# Maximum login retries.
|
||||
TINYAUTH_AUTH_LOGINMAXRETRIES=3
|
||||
# Comma-separated list of trusted proxy addresses.
|
||||
TINYAUTH_AUTH_TRUSTEDPROXIES=
|
||||
|
||||
# OAuth Configuration
|
||||
# apps config
|
||||
|
||||
# Regex pattern for allowed email addresses (e.g., /@example\.com$/)
|
||||
TINYAUTH_OAUTH_WHITELIST=""
|
||||
# Provider ID to auto-redirect to (skips login page)
|
||||
TINYAUTH_OAUTH_AUTOREDIRECT=""
|
||||
# OAuth Provider Configuration (replace MYPROVIDER with your provider name)
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_CLIENTID="your_client_id_here"
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_CLIENTSECRET="your_client_secret_here"
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_AUTHURL="https://provider.example.com/oauth/authorize"
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_TOKENURL="https://provider.example.com/oauth/token"
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_USERINFOURL="https://provider.example.com/oauth/userinfo"
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_REDIRECTURL="https://auth.example.com/oauth/callback/myprovider"
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_SCOPES="openid email profile"
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_NAME="My OAuth Provider"
|
||||
# Allow self-signed certificates
|
||||
TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_INSECURE="false"
|
||||
# The domain of the app.
|
||||
TINYAUTH_APPS_name_CONFIG_DOMAIN=
|
||||
# Comma-separated list of allowed users.
|
||||
TINYAUTH_APPS_name_USERS_ALLOW=
|
||||
# Comma-separated list of blocked users.
|
||||
TINYAUTH_APPS_name_USERS_BLOCK=
|
||||
# Comma-separated list of allowed OAuth groups.
|
||||
TINYAUTH_APPS_name_OAUTH_WHITELIST=
|
||||
# Comma-separated list of required OAuth groups.
|
||||
TINYAUTH_APPS_name_OAUTH_GROUPS=
|
||||
# List of allowed IPs or CIDR ranges.
|
||||
TINYAUTH_APPS_name_IP_ALLOW=
|
||||
# List of blocked IPs or CIDR ranges.
|
||||
TINYAUTH_APPS_name_IP_BLOCK=
|
||||
# List of IPs or CIDR ranges that bypass authentication.
|
||||
TINYAUTH_APPS_name_IP_BYPASS=
|
||||
# Custom headers to add to the response.
|
||||
TINYAUTH_APPS_name_RESPONSE_HEADERS=
|
||||
# Basic auth username.
|
||||
TINYAUTH_APPS_name_RESPONSE_BASICAUTH_USERNAME=
|
||||
# Basic auth password.
|
||||
TINYAUTH_APPS_name_RESPONSE_BASICAUTH_PASSWORD=
|
||||
# Path to the file containing the basic auth password.
|
||||
TINYAUTH_APPS_name_RESPONSE_BASICAUTH_PASSWORDFILE=
|
||||
# Comma-separated list of allowed paths.
|
||||
TINYAUTH_APPS_name_PATH_ALLOW=
|
||||
# Comma-separated list of blocked paths.
|
||||
TINYAUTH_APPS_name_PATH_BLOCK=
|
||||
# Comma-separated list of required LDAP groups.
|
||||
TINYAUTH_APPS_name_LDAP_GROUPS=
|
||||
|
||||
# UI Customization
|
||||
# oauth config
|
||||
|
||||
# Custom title for login page
|
||||
# Comma-separated list of allowed OAuth domains.
|
||||
TINYAUTH_OAUTH_WHITELIST=
|
||||
# The OAuth provider to use for automatic redirection.
|
||||
TINYAUTH_OAUTH_AUTOREDIRECT=
|
||||
# OAuth client ID.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_CLIENTID=
|
||||
# OAuth client secret.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_CLIENTSECRET=
|
||||
# Path to the file containing the OAuth client secret.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_CLIENTSECRETFILE=
|
||||
# OAuth scopes.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_SCOPES=
|
||||
# OAuth redirect URL.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_REDIRECTURL=
|
||||
# OAuth authorization URL.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_AUTHURL=
|
||||
# OAuth token URL.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_TOKENURL=
|
||||
# OAuth userinfo URL.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_USERINFOURL=
|
||||
# Allow insecure OAuth connections.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_INSECURE=false
|
||||
# Provider name in UI.
|
||||
TINYAUTH_OAUTH_PROVIDERS_name_NAME=
|
||||
|
||||
# oidc config
|
||||
|
||||
# Path to the private key file.
|
||||
TINYAUTH_OIDC_PRIVATEKEYPATH="./tinyauth_oidc_key"
|
||||
# Path to the public key file.
|
||||
TINYAUTH_OIDC_PUBLICKEYPATH="./tinyauth_oidc_key.pub"
|
||||
# OIDC client ID.
|
||||
TINYAUTH_OIDC_CLIENTS_name_CLIENTID=
|
||||
# OIDC client secret.
|
||||
TINYAUTH_OIDC_CLIENTS_name_CLIENTSECRET=
|
||||
# Path to the file containing the OIDC client secret.
|
||||
TINYAUTH_OIDC_CLIENTS_name_CLIENTSECRETFILE=
|
||||
# List of trusted redirect URIs.
|
||||
TINYAUTH_OIDC_CLIENTS_name_TRUSTEDREDIRECTURIS=
|
||||
# Client name in UI.
|
||||
TINYAUTH_OIDC_CLIENTS_name_NAME=
|
||||
|
||||
# ui config
|
||||
|
||||
# The title of the UI.
|
||||
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=""
|
||||
# Message displayed on the forgot password page.
|
||||
TINYAUTH_UI_FORGOTPASSWORDMESSAGE="You can change your password by changing the configuration."
|
||||
# Path to the background image.
|
||||
TINYAUTH_UI_BACKGROUNDIMAGE="/background.jpg"
|
||||
# Disable UI warnings.
|
||||
TINYAUTH_UI_DISABLEWARNINGS=false
|
||||
|
||||
# LDAP Configuration
|
||||
# ldap config
|
||||
|
||||
# LDAP server address
|
||||
TINYAUTH_LDAP_ADDRESS="ldap://ldap.example.com:389"
|
||||
# DN for binding to LDAP server
|
||||
TINYAUTH_LDAP_BINDDN="cn=readonly,dc=example,dc=com"
|
||||
# Password for bind DN
|
||||
TINYAUTH_LDAP_BINDPASSWORD="your_bind_password"
|
||||
# Base DN for user searches
|
||||
TINYAUTH_LDAP_BASEDN="dc=example,dc=com"
|
||||
# Search filter (%s will be replaced with username)
|
||||
TINYAUTH_LDAP_SEARCHFILTER="(&(uid=%s)(memberOf=cn=users,ou=groups,dc=example,dc=com))"
|
||||
# Allow insecure LDAP connections
|
||||
TINYAUTH_LDAP_INSECURE="false"
|
||||
# LDAP server address.
|
||||
TINYAUTH_LDAP_ADDRESS=
|
||||
# Bind DN for LDAP authentication.
|
||||
TINYAUTH_LDAP_BINDDN=
|
||||
# Bind password for LDAP authentication.
|
||||
TINYAUTH_LDAP_BINDPASSWORD=
|
||||
# Base DN for LDAP searches.
|
||||
TINYAUTH_LDAP_BASEDN=
|
||||
# Allow insecure LDAP connections.
|
||||
TINYAUTH_LDAP_INSECURE=false
|
||||
# LDAP search filter.
|
||||
TINYAUTH_LDAP_SEARCHFILTER="(uid=%s)"
|
||||
# Certificate for mTLS authentication.
|
||||
TINYAUTH_LDAP_AUTHCERT=
|
||||
# Certificate key for mTLS authentication.
|
||||
TINYAUTH_LDAP_AUTHKEY=
|
||||
# Cache duration for LDAP group membership in seconds.
|
||||
TINYAUTH_LDAP_GROUPCACHETTL=900
|
||||
|
||||
# log config
|
||||
|
||||
# Log level (trace, debug, info, warn, error).
|
||||
TINYAUTH_LOG_LEVEL="info"
|
||||
# Enable JSON formatted logs.
|
||||
TINYAUTH_LOG_JSON=false
|
||||
# Enable this log stream.
|
||||
TINYAUTH_LOG_STREAMS_HTTP_ENABLED=true
|
||||
# Log level for this stream. Use global if empty.
|
||||
TINYAUTH_LOG_STREAMS_HTTP_LEVEL=
|
||||
# Enable this log stream.
|
||||
TINYAUTH_LOG_STREAMS_APP_ENABLED=true
|
||||
# Log level for this stream. Use global if empty.
|
||||
TINYAUTH_LOG_STREAMS_APP_LEVEL=
|
||||
# Enable this log stream.
|
||||
TINYAUTH_LOG_STREAMS_AUDIT_ENABLED=false
|
||||
# Log level for this stream. Use global if empty.
|
||||
TINYAUTH_LOG_STREAMS_AUDIT_LEVEL=
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -42,3 +42,6 @@ __debug_*
|
||||
|
||||
# traefik data
|
||||
/traefik
|
||||
|
||||
# generated markdown (for docs)
|
||||
/config.gen.md
|
||||
|
||||
8
Makefile
8
Makefile
@@ -60,11 +60,11 @@ test:
|
||||
go test -v ./...
|
||||
|
||||
# Development
|
||||
develop:
|
||||
dev:
|
||||
docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build
|
||||
|
||||
# Development - Infisical
|
||||
develop-infisical:
|
||||
dev-infisical:
|
||||
infisical run --env=dev -- docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build
|
||||
|
||||
# Production
|
||||
@@ -79,3 +79,7 @@ prod-infisical:
|
||||
.PHONY: sql
|
||||
sql:
|
||||
sqlc generate
|
||||
|
||||
# Go gen
|
||||
generate:
|
||||
go run ./gen
|
||||
|
||||
@@ -12,60 +12,8 @@ import (
|
||||
"github.com/traefik/paerser/cli"
|
||||
)
|
||||
|
||||
func NewTinyauthCmdConfiguration() *config.Config {
|
||||
return &config.Config{
|
||||
ResourcesDir: "./resources",
|
||||
DatabasePath: "./tinyauth.db",
|
||||
Server: config.ServerConfig{
|
||||
Port: 3000,
|
||||
Address: "0.0.0.0",
|
||||
},
|
||||
Auth: config.AuthConfig{
|
||||
SessionExpiry: 86400, // 1 day
|
||||
SessionMaxLifetime: 0, // disabled
|
||||
LoginTimeout: 300, // 5 minutes
|
||||
LoginMaxRetries: 3,
|
||||
},
|
||||
UI: config.UIConfig{
|
||||
Title: "Tinyauth",
|
||||
ForgotPasswordMessage: "You can change your password by changing the configuration.",
|
||||
BackgroundImage: "/background.jpg",
|
||||
},
|
||||
Ldap: config.LdapConfig{
|
||||
Insecure: false,
|
||||
SearchFilter: "(uid=%s)",
|
||||
GroupCacheTTL: 900, // 15 minutes
|
||||
},
|
||||
Log: config.LogConfig{
|
||||
Level: "info",
|
||||
Json: false,
|
||||
Streams: config.LogStreams{
|
||||
HTTP: config.LogStreamConfig{
|
||||
Enabled: true,
|
||||
Level: "",
|
||||
},
|
||||
App: config.LogStreamConfig{
|
||||
Enabled: true,
|
||||
Level: "",
|
||||
},
|
||||
Audit: config.LogStreamConfig{
|
||||
Enabled: false,
|
||||
Level: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
OIDC: config.OIDCConfig{
|
||||
PrivateKeyPath: "./tinyauth_oidc_key",
|
||||
PublicKeyPath: "./tinyauth_oidc_key.pub",
|
||||
},
|
||||
Experimental: config.ExperimentalConfig{
|
||||
ConfigFile: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
tConfig := NewTinyauthCmdConfiguration()
|
||||
tConfig := config.NewDefaultConfiguration()
|
||||
|
||||
loaders := []cli.ResourceLoader{
|
||||
&loaders.FileLoader{},
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# Tinyauth Example Configuration
|
||||
|
||||
# The base URL where Tinyauth is accessible
|
||||
appUrl: "https://auth.example.com"
|
||||
# Directory for static resources
|
||||
resourcesDir: "./resources"
|
||||
# Path to SQLite database file
|
||||
databasePath: "./tinyauth.db"
|
||||
# Disable usage analytics
|
||||
disableAnalytics: false
|
||||
# Disable static resource serving
|
||||
disableResources: false
|
||||
# Disable UI warning messages
|
||||
disableUIWarnings: false
|
||||
|
||||
# Logging Configuration
|
||||
log:
|
||||
# Log level: trace, debug, info, warn, error
|
||||
level: "info"
|
||||
json: false
|
||||
streams:
|
||||
app:
|
||||
enabled: true
|
||||
level: "warn"
|
||||
http:
|
||||
enabled: true
|
||||
level: "debug"
|
||||
audit:
|
||||
enabled: false
|
||||
level: "info"
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
# Port to listen on
|
||||
port: 3000
|
||||
# Interface to bind to (0.0.0.0 for all interfaces)
|
||||
address: "0.0.0.0"
|
||||
# Unix socket path (optional, overrides port/address if set)
|
||||
socketPath: ""
|
||||
# Comma-separated list of trusted proxy IPs/CIDRs
|
||||
trustedProxies: ""
|
||||
|
||||
# Authentication Configuration
|
||||
auth:
|
||||
# Format: username:bcrypt_hash (use bcrypt to generate hash)
|
||||
users: "admin:$2a$10$example_bcrypt_hash_here"
|
||||
# Path to external users file (optional)
|
||||
usersFile: ""
|
||||
# Enable secure cookies (requires HTTPS)
|
||||
secureCookie: false
|
||||
# Session expiry in seconds (3600 = 1 hour)
|
||||
sessionExpiry: 3600
|
||||
# Session maximum lifetime in seconds (0 = unlimited)
|
||||
sessionMaxLifetime: 0
|
||||
# Login timeout in seconds (300 = 5 minutes)
|
||||
loginTimeout: 300
|
||||
# Maximum login retries before lockout
|
||||
loginMaxRetries: 3
|
||||
|
||||
# OAuth Configuration
|
||||
oauth:
|
||||
# Regex pattern for allowed email addresses (e.g., /@example\.com$/)
|
||||
whitelist: ""
|
||||
# Provider ID to auto-redirect to (skips login page)
|
||||
autoRedirect: ""
|
||||
# OAuth Provider Configuration (replace myprovider with your provider name)
|
||||
providers:
|
||||
myprovider:
|
||||
clientId: "your_client_id_here"
|
||||
clientSecret: "your_client_secret_here"
|
||||
authUrl: "https://provider.example.com/oauth/authorize"
|
||||
tokenUrl: "https://provider.example.com/oauth/token"
|
||||
userInfoUrl: "https://provider.example.com/oauth/userinfo"
|
||||
redirectUrl: "https://auth.example.com/api/oauth/callback/myprovider"
|
||||
scopes: "openid email profile"
|
||||
name: "My OAuth Provider"
|
||||
# Allow insecure connections (self-signed certificates)
|
||||
insecure: false
|
||||
|
||||
# UI Customization
|
||||
ui:
|
||||
# Custom title for login page
|
||||
title: "Tinyauth"
|
||||
# Message shown on forgot password page
|
||||
forgotPasswordMessage: "Contact your administrator to reset your password"
|
||||
# Background image URL for login page
|
||||
backgroundImage: ""
|
||||
|
||||
# LDAP Configuration (optional)
|
||||
ldap:
|
||||
# LDAP server address
|
||||
address: "ldap://ldap.example.com:389"
|
||||
# DN for binding to LDAP server
|
||||
bindDn: "cn=readonly,dc=example,dc=com"
|
||||
# Password for bind DN
|
||||
bindPassword: "your_bind_password"
|
||||
# Base DN for user searches
|
||||
baseDn: "dc=example,dc=com"
|
||||
# Search filter (%s will be replaced with username)
|
||||
searchFilter: "(&(uid=%s)(memberOf=cn=users,ou=groups,dc=example,dc=com))"
|
||||
# Allow insecure LDAP connections
|
||||
insecure: false
|
||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -22,3 +22,6 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Stats out
|
||||
stats.html
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.5.0",
|
||||
"globals": "^17.3.0",
|
||||
"prettier": "3.8.1",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.55.0",
|
||||
@@ -401,6 +402,10 @@
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
@@ -431,8 +436,14 @@
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
@@ -451,6 +462,8 @@
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
@@ -465,6 +478,8 @@
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
@@ -531,6 +546,8 @@
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||
@@ -583,14 +600,20 @@
|
||||
|
||||
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
||||
|
||||
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||
@@ -721,6 +744,8 @@
|
||||
|
||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||
|
||||
"open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
@@ -773,8 +798,12 @@
|
||||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"rollup": ["rollup@4.46.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.2", "@rollup/rollup-android-arm64": "4.46.2", "@rollup/rollup-darwin-arm64": "4.46.2", "@rollup/rollup-darwin-x64": "4.46.2", "@rollup/rollup-freebsd-arm64": "4.46.2", "@rollup/rollup-freebsd-x64": "4.46.2", "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", "@rollup/rollup-linux-arm-musleabihf": "4.46.2", "@rollup/rollup-linux-arm64-gnu": "4.46.2", "@rollup/rollup-linux-arm64-musl": "4.46.2", "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", "@rollup/rollup-linux-ppc64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-musl": "4.46.2", "@rollup/rollup-linux-s390x-gnu": "4.46.2", "@rollup/rollup-linux-x64-gnu": "4.46.2", "@rollup/rollup-linux-x64-musl": "4.46.2", "@rollup/rollup-win32-arm64-msvc": "4.46.2", "@rollup/rollup-win32-ia32-msvc": "4.46.2", "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg=="],
|
||||
|
||||
"rollup-plugin-visualizer": ["rollup-plugin-visualizer@6.0.5", "", { "dependencies": { "open": "^8.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^17.5.1" }, "peerDependencies": { "rolldown": "1.x || ^1.0.0-beta", "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rolldown", "rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-9+HlNgKCVbJDs8tVtjQ43US12eqaiHyyiLMdBwQ7vSZPiHMysGNo2E88TAp1si5wx8NAoYriI2A5kuKfIakmJg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
@@ -787,12 +816,18 @@
|
||||
|
||||
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.16", "", { "dependencies": { "style-to-object": "1.0.8" } }, "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
|
||||
@@ -857,8 +892,16 @@
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.5.0",
|
||||
"globals": "^17.3.0",
|
||||
"prettier": "3.8.1",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.55.0",
|
||||
|
||||
@@ -2,15 +2,43 @@ import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { visualizer } from "rollup-plugin-visualizer";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
plugins: [react(), tailwindcss(), visualizer()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes("node_modules")) {
|
||||
if (id.includes("/react")) {
|
||||
return "vendor-react";
|
||||
}
|
||||
|
||||
if (id.includes("/@radix-ui")) {
|
||||
return "vendor-radix";
|
||||
}
|
||||
|
||||
if (id.includes("/i18next")) {
|
||||
return "vendor-i18next";
|
||||
}
|
||||
|
||||
if (id.includes("/zod")) {
|
||||
return "vendor-zod";
|
||||
}
|
||||
|
||||
return "vendor";
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
proxy: {
|
||||
|
||||
38
gen/gen.go
Normal file
38
gen/gen.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func main() {
|
||||
slog.Info("generating example env file")
|
||||
generateExampleEnv()
|
||||
slog.Info("generating config reference markdown file")
|
||||
generateMarkdown()
|
||||
}
|
||||
|
||||
func walkAndBuild[T any](parent reflect.Type, parentValue reflect.Value,
|
||||
parentPath string, entries *[]T,
|
||||
buildEntry func(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]T),
|
||||
buildMap func(child reflect.StructField, parentPath string, entries *[]T),
|
||||
buildChildPath func(parentPath string, childName string) string,
|
||||
) {
|
||||
for i := 0; i < parent.NumField(); i++ {
|
||||
field := parent.Field(i)
|
||||
fieldType := field.Type
|
||||
fieldValue := parentValue.Field(i)
|
||||
|
||||
switch fieldType.Kind() {
|
||||
case reflect.Struct:
|
||||
childPath := buildChildPath(parentPath, field.Name)
|
||||
walkAndBuild[T](fieldType, fieldValue, childPath, entries, buildEntry, buildMap, buildChildPath)
|
||||
case reflect.Map:
|
||||
buildMap(field, parentPath, entries)
|
||||
case reflect.Bool, reflect.String, reflect.Slice, reflect.Int:
|
||||
buildEntry(field, fieldValue, parentPath, entries)
|
||||
default:
|
||||
slog.Info("unknown type", "type", fieldType.Kind())
|
||||
}
|
||||
}
|
||||
}
|
||||
131
gen/gen_env.go
Normal file
131
gen/gen_env.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
)
|
||||
|
||||
type EnvEntry struct {
|
||||
Name string
|
||||
Description string
|
||||
Value any
|
||||
}
|
||||
|
||||
func generateExampleEnv() {
|
||||
cfg := config.NewDefaultConfiguration()
|
||||
entries := make([]EnvEntry, 0)
|
||||
|
||||
root := reflect.TypeOf(cfg).Elem()
|
||||
rootValue := reflect.ValueOf(cfg).Elem()
|
||||
rootPath := "TINYAUTH_"
|
||||
|
||||
walkAndBuild(root, rootValue, rootPath, &entries, buildEnvEntry, buildEnvMapEntry, buildEnvChildPath)
|
||||
compiled := compileEnv(entries)
|
||||
|
||||
err := os.Remove(".env.example")
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
slog.Error("failed to remove example env file", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = os.WriteFile(".env.example", compiled, 0644)
|
||||
if err != nil {
|
||||
slog.Error("failed to write example env file", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func buildEnvEntry(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]EnvEntry) {
|
||||
desc := child.Tag.Get("description")
|
||||
tag := child.Tag.Get("yaml")
|
||||
|
||||
if tag == "-" {
|
||||
return
|
||||
}
|
||||
|
||||
value := childValue.Interface()
|
||||
|
||||
entry := EnvEntry{
|
||||
Name: parentPath + strings.ToUpper(child.Name),
|
||||
Description: desc,
|
||||
}
|
||||
|
||||
switch childValue.Kind() {
|
||||
case reflect.Slice:
|
||||
sl, ok := value.([]string)
|
||||
if !ok {
|
||||
slog.Error("invalid default value", "value", value)
|
||||
return
|
||||
}
|
||||
entry.Value = strings.Join(sl, ",")
|
||||
case reflect.String:
|
||||
st, ok := value.(string)
|
||||
if !ok {
|
||||
slog.Error("invalid default value", "value", value)
|
||||
return
|
||||
}
|
||||
if st != "" {
|
||||
entry.Value = fmt.Sprintf(`"%s"`, st)
|
||||
} else {
|
||||
entry.Value = ""
|
||||
}
|
||||
default:
|
||||
entry.Value = value
|
||||
}
|
||||
*entries = append(*entries, entry)
|
||||
}
|
||||
|
||||
func buildEnvMapEntry(child reflect.StructField, parentPath string, entries *[]EnvEntry) {
|
||||
fieldType := child.Type
|
||||
|
||||
if fieldType.Key().Kind() != reflect.String {
|
||||
slog.Info("unsupported map key type", "type", fieldType.Key().Kind())
|
||||
return
|
||||
}
|
||||
|
||||
mapPath := parentPath + strings.ToUpper(child.Name) + "_name_"
|
||||
valueType := fieldType.Elem()
|
||||
|
||||
if valueType.Kind() == reflect.Struct {
|
||||
zeroValue := reflect.New(valueType).Elem()
|
||||
walkAndBuild(valueType, zeroValue, mapPath, entries, buildEnvEntry, buildEnvMapEntry, buildEnvChildPath)
|
||||
}
|
||||
}
|
||||
|
||||
func buildEnvChildPath(parent string, child string) string {
|
||||
return parent + strings.ToUpper(child) + "_"
|
||||
}
|
||||
|
||||
func compileEnv(entries []EnvEntry) []byte {
|
||||
buffer := bytes.Buffer{}
|
||||
buffer.WriteString("# Tinyauth example configuration\n\n")
|
||||
|
||||
previousSection := ""
|
||||
|
||||
for _, entry := range entries {
|
||||
if strings.Count(entry.Name, "_") > 1 {
|
||||
section := strings.Split(strings.TrimPrefix(entry.Name, "TINYAUTH_"), "_")[0]
|
||||
if section != previousSection {
|
||||
buffer.WriteString("\n# " + strings.ToLower(section) + " config\n\n")
|
||||
previousSection = section
|
||||
}
|
||||
}
|
||||
buffer.WriteString("# ")
|
||||
buffer.WriteString(entry.Description)
|
||||
buffer.WriteString("\n")
|
||||
buffer.WriteString(entry.Name)
|
||||
buffer.WriteString("=")
|
||||
fmt.Fprintf(&buffer, "%v", entry.Value)
|
||||
buffer.WriteString("\n")
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
}
|
||||
127
gen/gen_md.go
Normal file
127
gen/gen_md.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
)
|
||||
|
||||
type MarkdownEntry struct {
|
||||
Env string
|
||||
Flag string
|
||||
Description string
|
||||
Default any
|
||||
}
|
||||
|
||||
func generateMarkdown() {
|
||||
cfg := config.NewDefaultConfiguration()
|
||||
entries := make([]MarkdownEntry, 0)
|
||||
|
||||
root := reflect.TypeOf(cfg).Elem()
|
||||
rootValue := reflect.ValueOf(cfg).Elem()
|
||||
rootPath := "tinyauth."
|
||||
|
||||
walkAndBuild(root, rootValue, rootPath, &entries, buildMdEntry, buildMdMapEntry, buildMdChildPath)
|
||||
compiled := compileMd(entries)
|
||||
|
||||
err := os.Remove("config.gen.md")
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
slog.Error("failed to remove example env file", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = os.WriteFile("config.gen.md", compiled, 0644)
|
||||
if err != nil {
|
||||
slog.Error("failed to write example env file", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func buildMdEntry(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]MarkdownEntry) {
|
||||
desc := child.Tag.Get("description")
|
||||
tag := child.Tag.Get("yaml")
|
||||
|
||||
if tag == "-" {
|
||||
return
|
||||
}
|
||||
|
||||
value := childValue.Interface()
|
||||
|
||||
entry := MarkdownEntry{
|
||||
Env: strings.ToUpper(strings.ReplaceAll(parentPath, ".", "_")) + strings.ToUpper(child.Name),
|
||||
Flag: fmt.Sprintf("--%s%s", strings.TrimPrefix(parentPath, "tinyauth."), tag),
|
||||
Description: desc,
|
||||
}
|
||||
|
||||
switch childValue.Kind() {
|
||||
case reflect.Slice:
|
||||
sl, ok := value.([]string)
|
||||
if !ok {
|
||||
slog.Error("invalid default value", "value", value)
|
||||
return
|
||||
}
|
||||
entry.Default = fmt.Sprintf("`%s`", strings.Join(sl, ","))
|
||||
default:
|
||||
entry.Default = fmt.Sprintf("`%v`", value)
|
||||
}
|
||||
*entries = append(*entries, entry)
|
||||
}
|
||||
|
||||
func buildMdMapEntry(child reflect.StructField, parentPath string, entries *[]MarkdownEntry) {
|
||||
fieldType := child.Type
|
||||
|
||||
if fieldType.Key().Kind() != reflect.String {
|
||||
slog.Info("unsupported map key type", "type", fieldType.Key().Kind())
|
||||
return
|
||||
}
|
||||
|
||||
tag := child.Tag.Get("yaml")
|
||||
|
||||
if tag == "-" {
|
||||
return
|
||||
}
|
||||
|
||||
mapPath := parentPath + tag + ".[name]."
|
||||
valueType := fieldType.Elem()
|
||||
|
||||
if valueType.Kind() == reflect.Struct {
|
||||
zeroValue := reflect.New(valueType).Elem()
|
||||
walkAndBuild(valueType, zeroValue, mapPath, entries, buildMdEntry, buildMdMapEntry, buildMdChildPath)
|
||||
}
|
||||
}
|
||||
|
||||
func buildMdChildPath(parent string, child string) string {
|
||||
return parent + strings.ToLower(child) + "."
|
||||
}
|
||||
|
||||
func compileMd(entries []MarkdownEntry) []byte {
|
||||
buffer := bytes.Buffer{}
|
||||
|
||||
buffer.WriteString("# Tinyauth configuration reference\n\n")
|
||||
buffer.WriteString("| Environment | Flag | Description | Default |\n")
|
||||
buffer.WriteString("| - | - | - | - |\n")
|
||||
|
||||
previousSection := ""
|
||||
|
||||
for _, entry := range entries {
|
||||
if strings.Count(entry.Env, "_") > 1 {
|
||||
section := strings.Split(strings.TrimPrefix(entry.Env, "TINYAUTH_"), "_")[0]
|
||||
if section != previousSection {
|
||||
buffer.WriteString("\n## " + strings.ToLower(section) + "\n\n")
|
||||
buffer.WriteString("| Environment | Flag | Description | Default |\n")
|
||||
buffer.WriteString("| - | - | - | - |\n")
|
||||
previousSection = section
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&buffer, "| `%s` | `%s` | %s | %s |\n", entry.Env, entry.Flag, entry.Description, entry.Default)
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
}
|
||||
@@ -21,8 +21,8 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||
engine := gin.New()
|
||||
engine.Use(gin.Recovery())
|
||||
|
||||
if len(app.config.Server.TrustedProxies) > 0 {
|
||||
err := engine.SetTrustedProxies(app.config.Server.TrustedProxies)
|
||||
if len(app.config.Auth.TrustedProxies) > 0 {
|
||||
err := engine.SetTrustedProxies(app.config.Auth.TrustedProxies)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set trusted proxies: %w", err)
|
||||
@@ -71,7 +71,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||
ForgotPasswordMessage: app.config.UI.ForgotPasswordMessage,
|
||||
BackgroundImage: app.config.UI.BackgroundImage,
|
||||
OAuthAutoRedirect: app.config.OAuth.AutoRedirect,
|
||||
DisableUIWarnings: app.config.DisableUIWarnings,
|
||||
DisableUIWarnings: app.config.UI.DisableWarnings,
|
||||
}, apiRouter)
|
||||
|
||||
contextController.SetupRoutes()
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
package config
|
||||
|
||||
// Default configuration
|
||||
func NewDefaultConfiguration() *Config {
|
||||
return &Config{
|
||||
ResourcesDir: "./resources",
|
||||
DatabasePath: "./tinyauth.db",
|
||||
Server: ServerConfig{
|
||||
Port: 3000,
|
||||
Address: "0.0.0.0",
|
||||
},
|
||||
Auth: AuthConfig{
|
||||
SessionExpiry: 86400, // 1 day
|
||||
SessionMaxLifetime: 0, // disabled
|
||||
LoginTimeout: 300, // 5 minutes
|
||||
LoginMaxRetries: 3,
|
||||
},
|
||||
UI: UIConfig{
|
||||
Title: "Tinyauth",
|
||||
ForgotPasswordMessage: "You can change your password by changing the configuration.",
|
||||
BackgroundImage: "/background.jpg",
|
||||
},
|
||||
Ldap: LdapConfig{
|
||||
Insecure: false,
|
||||
SearchFilter: "(uid=%s)",
|
||||
GroupCacheTTL: 900, // 15 minutes
|
||||
},
|
||||
Log: LogConfig{
|
||||
Level: "info",
|
||||
Json: false,
|
||||
Streams: LogStreams{
|
||||
HTTP: LogStreamConfig{
|
||||
Enabled: true,
|
||||
Level: "",
|
||||
},
|
||||
App: LogStreamConfig{
|
||||
Enabled: true,
|
||||
Level: "",
|
||||
},
|
||||
Audit: LogStreamConfig{
|
||||
Enabled: false,
|
||||
Level: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
OIDC: OIDCConfig{
|
||||
PrivateKeyPath: "./tinyauth_oidc_key",
|
||||
PublicKeyPath: "./tinyauth_oidc_key.pub",
|
||||
},
|
||||
Experimental: ExperimentalConfig{
|
||||
ConfigFile: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Version information, set at build time
|
||||
|
||||
var Version = "development"
|
||||
@@ -15,28 +68,26 @@ var RedirectCookieName = "tinyauth-redirect"
|
||||
// Main app config
|
||||
|
||||
type Config struct {
|
||||
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
||||
ResourcesDir string `description:"The directory where resources are stored." yaml:"resourcesDir"`
|
||||
DatabasePath string `description:"The path to the database file." yaml:"databasePath"`
|
||||
DisableAnalytics bool `description:"Disable analytics." yaml:"disableAnalytics"`
|
||||
DisableResources bool `description:"Disable resources server." yaml:"disableResources"`
|
||||
DisableUIWarnings bool `description:"Disable UI warnings." yaml:"disableUIWarnings"`
|
||||
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
||||
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
||||
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
||||
ResourcesDir string `description:"The directory where resources are stored." yaml:"resourcesDir"`
|
||||
DatabasePath string `description:"The path to the database file." yaml:"databasePath"`
|
||||
DisableAnalytics bool `description:"Disable analytics." yaml:"disableAnalytics"`
|
||||
DisableResources bool `description:"Disable resources server." yaml:"disableResources"`
|
||||
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
||||
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
||||
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `description:"The port on which the server listens." yaml:"port"`
|
||||
Address string `description:"The address on which the server listens." yaml:"address"`
|
||||
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
|
||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||
Port int `description:"The port on which the server listens." yaml:"port"`
|
||||
Address string `description:"The address on which the server listens." yaml:"address"`
|
||||
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
@@ -48,6 +99,7 @@ type AuthConfig struct {
|
||||
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"`
|
||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||
}
|
||||
|
||||
type IPConfig struct {
|
||||
@@ -71,6 +123,7 @@ type UIConfig struct {
|
||||
Title string `description:"The title of the UI." yaml:"title"`
|
||||
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"`
|
||||
BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage"`
|
||||
DisableWarnings bool `description:"Disable UI warnings." yaml:"disableWarnings"`
|
||||
}
|
||||
|
||||
type LdapConfig struct {
|
||||
|
||||
Reference in New Issue
Block a user