mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-01-15 20:02:30 +00:00
Compare commits
8 Commits
refactor/u
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0dd432029 | ||
|
|
a0ef7b80d8 | ||
|
|
53bd413046 | ||
|
|
ba2d732415 | ||
|
|
0025485e38 | ||
|
|
3cba90f99c | ||
|
|
a842e40903 | ||
|
|
e3f92ce4fc |
17
.env.example
17
.env.example
@@ -2,8 +2,6 @@
|
||||
|
||||
# The base URL where Tinyauth is accessible
|
||||
TINYAUTH_APPURL="https://auth.example.com"
|
||||
# Log level: trace, debug, info, warn, error
|
||||
TINYAUTH_LOGLEVEL="info"
|
||||
# Directory for static resources
|
||||
TINYAUTH_RESOURCESDIR="/data/resources"
|
||||
# Path to SQLite database file
|
||||
@@ -14,8 +12,21 @@ TINYAUTH_DISABLEANALYTICS="false"
|
||||
TINYAUTH_DISABLERESOURCES="false"
|
||||
# Disable UI warning messages
|
||||
TINYAUTH_DISABLEUIWARNINGS="false"
|
||||
|
||||
# Logging Configuration
|
||||
|
||||
# Log level: trace, debug, info, warn, error
|
||||
TINYAUTH_LOG_LEVEL="info"
|
||||
# Enable JSON formatted logs
|
||||
TINYAUTH_LOGJSON="false"
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Site builder
|
||||
FROM oven/bun:1.3.5-alpine AS frontend-builder
|
||||
FROM oven/bun:1.3.6-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Site builder
|
||||
FROM oven/bun:1.3.5-alpine AS frontend-builder
|
||||
FROM oven/bun:1.3.6-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
|
||||
|
||||
A big thank you to the following people for providing me with more coffee:
|
||||
|
||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <!-- sponsors -->
|
||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/algorist-ahmad"><img src="https://github.com/algorist-ahmad.png" width="64px" alt="User avatar: algorist-ahmad" /></a> <!-- sponsors -->
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
@@ -3,13 +3,10 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/traefik/paerser/cli"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -43,7 +40,7 @@ func createUserCmd() *cli.Command {
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
@@ -77,7 +74,7 @@ func createUserCmd() *cli.Command {
|
||||
return errors.New("username and password cannot be empty")
|
||||
}
|
||||
|
||||
log.Info().Str("username", tCfg.Username).Msg("Creating user")
|
||||
tlog.App.Info().Str("username", tCfg.Username).Msg("Creating user")
|
||||
|
||||
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -90,7 +87,7 @@ func createUserCmd() *cli.Command {
|
||||
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||
}
|
||||
|
||||
log.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
|
||||
tlog.App.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -5,15 +5,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/mdp/qrterminal/v3"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/paerser/cli"
|
||||
)
|
||||
|
||||
@@ -42,7 +40,7 @@ func generateTotpCmd() *cli.Command {
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
@@ -91,9 +89,9 @@ func generateTotpCmd() *cli.Command {
|
||||
|
||||
secret := key.Secret()
|
||||
|
||||
log.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
||||
tlog.App.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
||||
|
||||
log.Info().Msg("Generated QR code")
|
||||
tlog.App.Info().Msg("Generated QR code")
|
||||
|
||||
config := qrterminal.Config{
|
||||
Level: qrterminal.L,
|
||||
@@ -112,7 +110,7 @@ func generateTotpCmd() *cli.Command {
|
||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||
}
|
||||
|
||||
log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
||||
tlog.App.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -9,8 +9,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/traefik/paerser/cli"
|
||||
)
|
||||
|
||||
@@ -27,7 +26,7 @@ func healthcheckCmd() *cli.Command {
|
||||
Resources: nil,
|
||||
AllowArg: true,
|
||||
Run: func(args []string) error {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
appUrl := os.Getenv("TINYAUTH_APPURL")
|
||||
|
||||
@@ -39,7 +38,7 @@ func healthcheckCmd() *cli.Command {
|
||||
return errors.New("TINYAUTH_APPURL is not set and no argument was provided")
|
||||
}
|
||||
|
||||
log.Info().Str("app_url", appUrl).Msg("Performing health check")
|
||||
tlog.App.Info().Str("app_url", appUrl).Msg("Performing health check")
|
||||
|
||||
client := http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
@@ -77,7 +76,7 @@ func healthcheckCmd() *cli.Command {
|
||||
return fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Interface("response", healthResp).Msg("Tinyauth is healthy")
|
||||
tlog.App.Info().Interface("response", healthResp).Msg("Tinyauth is healthy")
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -2,22 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/loaders"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/paerser/cli"
|
||||
)
|
||||
|
||||
func NewTinyauthCmdConfiguration() *config.Config {
|
||||
return &config.Config{
|
||||
LogLevel: "info",
|
||||
ResourcesDir: "./resources",
|
||||
DatabasePath: "./tinyauth.db",
|
||||
Server: config.ServerConfig{
|
||||
@@ -39,6 +35,24 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
||||
Insecure: false,
|
||||
SearchFilter: "(uid=%s)",
|
||||
},
|
||||
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: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Experimental: config.ExperimentalConfig{
|
||||
ConfigFile: "",
|
||||
},
|
||||
@@ -102,25 +116,14 @@ func main() {
|
||||
}
|
||||
|
||||
func runCmd(cfg config.Config) error {
|
||||
logLevel, err := zerolog.ParseLevel(strings.ToLower(cfg.LogLevel))
|
||||
logger := tlog.NewLogger(cfg.Log)
|
||||
logger.Init()
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Invalid or missing log level, defaulting to info")
|
||||
} else {
|
||||
zerolog.SetGlobalLevel(logLevel)
|
||||
}
|
||||
|
||||
log.Logger = log.With().Caller().Logger()
|
||||
|
||||
if !cfg.LogJSON {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
|
||||
}
|
||||
|
||||
log.Info().Str("version", config.Version).Msg("Starting tinyauth")
|
||||
tlog.App.Info().Str("version", config.Version).Msg("Starting tinyauth")
|
||||
|
||||
app := bootstrap.NewBootstrapApp(cfg)
|
||||
|
||||
err = app.Setup()
|
||||
err := app.Setup()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to bootstrap app: %w", err)
|
||||
|
||||
@@ -3,15 +3,12 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/paerser/cli"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -47,7 +44,7 @@ func verifyUserCmd() *cli.Command {
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
@@ -101,9 +98,9 @@ func verifyUserCmd() *cli.Command {
|
||||
|
||||
if user.TotpSecret == "" {
|
||||
if tCfg.Totp != "" {
|
||||
log.Warn().Msg("User does not have TOTP secret")
|
||||
tlog.App.Warn().Msg("User does not have TOTP secret")
|
||||
}
|
||||
log.Info().Msg("User verified")
|
||||
tlog.App.Info().Msg("User verified")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -113,7 +110,7 @@ func verifyUserCmd() *cli.Command {
|
||||
return fmt.Errorf("TOTP code incorrect")
|
||||
}
|
||||
|
||||
log.Info().Msg("User verified")
|
||||
tlog.App.Info().Msg("User verified")
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
# The base URL where Tinyauth is accessible
|
||||
appUrl: "https://auth.example.com"
|
||||
# Log level: trace, debug, info, warn, error
|
||||
logLevel: "info"
|
||||
# Directory for static resources
|
||||
resourcesDir: "./resources"
|
||||
# Path to SQLite database file
|
||||
@@ -14,8 +12,22 @@ disableAnalytics: false
|
||||
disableResources: false
|
||||
# Disable UI warning messages
|
||||
disableUIWarnings: false
|
||||
# Enable JSON formatted logs
|
||||
logJSON: 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:
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -24,8 +24,8 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.70.0",
|
||||
"react-i18next": "^16.5.1",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-i18next": "^16.5.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.12.0",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -36,18 +36,18 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/node": "^25.0.8",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"globals": "^17.0.0",
|
||||
"prettier": "3.7.4",
|
||||
"prettier": "3.8.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.52.0",
|
||||
"typescript-eslint": "^8.53.0",
|
||||
"vite": "^7.3.1",
|
||||
},
|
||||
},
|
||||
@@ -339,9 +339,9 @@
|
||||
|
||||
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.16", "", {}, "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww=="],
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.17", "", {}, "sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.16", "", { "dependencies": { "@tanstack/query-core": "5.90.16" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ=="],
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.17", "", { "dependencies": { "@tanstack/query-core": "5.90.17" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
@@ -365,33 +365,33 @@
|
||||
|
||||
"@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@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="],
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.0", "@typescript-eslint/types": "^8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg=="],
|
||||
|
||||
"@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.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="],
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.0", "@typescript-eslint/tsconfig-utils": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw=="],
|
||||
|
||||
"@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.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="],
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
@@ -781,7 +781,7 @@
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||
"prettier": ["prettier@3.8.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
@@ -795,9 +795,9 @@
|
||||
|
||||
"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-hook-form": ["react-hook-form@7.70.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw=="],
|
||||
"react-hook-form": ["react-hook-form@7.71.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w=="],
|
||||
|
||||
"react-i18next": ["react-i18next@16.5.1", "", { "dependencies": { "@babel/runtime": "^7.28.4", "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-Hks6UIRZWW4c+qDAnx1csVsCGYeIR4MoBGQgJ+NUoNnO6qLxXuf8zu0xdcinyXUORgGzCdRsexxO1Xzv3sTdnw=="],
|
||||
"react-i18next": ["react-i18next@16.5.3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "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-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -873,7 +873,7 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.52.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/parser": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.53.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -997,31 +997,31 @@
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="],
|
||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
|
||||
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
||||
|
||||
"@typescript-eslint/parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
|
||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
||||
|
||||
"@typescript-eslint/project-service/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@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.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="],
|
||||
|
||||
"@typescript-eslint/type-utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
|
||||
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
@@ -1031,7 +1031,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.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
|
||||
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1049,7 +1049,7 @@
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="],
|
||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -1067,15 +1067,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.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
@@ -1099,9 +1099,9 @@
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="],
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -30,8 +30,8 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.70.0",
|
||||
"react-i18next": "^16.5.1",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-i18next": "^16.5.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.12.0",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -42,18 +42,18 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/node": "^25.0.8",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"globals": "^17.0.0",
|
||||
"prettier": "3.7.4",
|
||||
"prettier": "3.8.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.52.0",
|
||||
"typescript-eslint": "^8.53.0",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
16
go.mod
16
go.mod
@@ -20,11 +20,11 @@ require (
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/traefik/paerser v0.2.2
|
||||
github.com/weppos/publicsuffix-go v0.50.2
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
gotest.tools/v3 v3.5.2
|
||||
modernc.org/sqlite v1.42.2
|
||||
modernc.org/sqlite v1.44.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -91,7 +91,7 @@ require (
|
||||
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/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
@@ -114,12 +114,12 @@ require (
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
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
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/libc v1.67.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
|
||||
52
go.sum
52
go.sum
@@ -140,6 +140,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3Ar
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
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=
|
||||
@@ -215,8 +217,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/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=
|
||||
@@ -299,13 +301,13 @@ golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -328,26 +330,26 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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=
|
||||
@@ -369,18 +371,20 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
|
||||
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -389,8 +393,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
|
||||
modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
||||
modernc.org/sqlite v1.44.0 h1:YjCKJnzZde2mLVy0cMKTSL4PxCmbIguOq9lGp8ZvGOc=
|
||||
modernc.org/sqlite v1.44.0/go.mod h1:2Dq41ir5/qri7QJJJKNZcP4UF7TsX/KNeykYgPDtGhE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -16,8 +16,7 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
)
|
||||
|
||||
type BootstrapApp struct {
|
||||
@@ -103,13 +102,13 @@ func (app *BootstrapApp) Setup() error {
|
||||
app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
||||
|
||||
// Dumps
|
||||
log.Trace().Interface("config", app.config).Msg("Config dump")
|
||||
log.Trace().Interface("users", app.context.users).Msg("Users dump")
|
||||
log.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
||||
log.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
||||
log.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
||||
log.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
|
||||
log.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
|
||||
tlog.App.Trace().Interface("config", app.config).Msg("Config dump")
|
||||
tlog.App.Trace().Interface("users", app.context.users).Msg("Users dump")
|
||||
tlog.App.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
||||
tlog.App.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
||||
tlog.App.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
||||
tlog.App.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
|
||||
tlog.App.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
|
||||
|
||||
// Database
|
||||
db, err := app.SetupDatabase(app.config.DatabasePath)
|
||||
@@ -153,7 +152,7 @@ func (app *BootstrapApp) Setup() error {
|
||||
})
|
||||
}
|
||||
|
||||
log.Debug().Interface("providers", configuredProviders).Msg("Authentication providers")
|
||||
tlog.App.Debug().Interface("providers", configuredProviders).Msg("Authentication providers")
|
||||
|
||||
if len(configuredProviders) == 0 {
|
||||
return fmt.Errorf("no authentication providers configured")
|
||||
@@ -169,28 +168,28 @@ func (app *BootstrapApp) Setup() error {
|
||||
}
|
||||
|
||||
// Start db cleanup routine
|
||||
log.Debug().Msg("Starting database cleanup routine")
|
||||
tlog.App.Debug().Msg("Starting database cleanup routine")
|
||||
go app.dbCleanup(queries)
|
||||
|
||||
// If analytics are not disabled, start heartbeat
|
||||
if !app.config.DisableAnalytics {
|
||||
log.Debug().Msg("Starting heartbeat routine")
|
||||
tlog.App.Debug().Msg("Starting heartbeat routine")
|
||||
go app.heartbeat()
|
||||
}
|
||||
|
||||
// If we have an socket path, bind to it
|
||||
if app.config.Server.SocketPath != "" {
|
||||
if _, err := os.Stat(app.config.Server.SocketPath); err == nil {
|
||||
log.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
|
||||
tlog.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
|
||||
err := os.Remove(app.config.Server.SocketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove existing socket file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
|
||||
tlog.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
|
||||
if err := router.RunUnix(app.config.Server.SocketPath); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start server")
|
||||
tlog.App.Fatal().Err(err).Msg("Failed to start server")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -198,9 +197,9 @@ func (app *BootstrapApp) Setup() error {
|
||||
|
||||
// Start server
|
||||
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
||||
log.Info().Msgf("Starting server on %s", address)
|
||||
tlog.App.Info().Msgf("Starting server on %s", address)
|
||||
if err := router.Run(address); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start server")
|
||||
tlog.App.Fatal().Err(err).Msg("Failed to start server")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -223,7 +222,7 @@ func (app *BootstrapApp) heartbeat() {
|
||||
bodyJson, err := json.Marshal(body)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to marshal heartbeat body")
|
||||
tlog.App.Error().Err(err).Msg("Failed to marshal heartbeat body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -234,12 +233,12 @@ func (app *BootstrapApp) heartbeat() {
|
||||
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
||||
|
||||
for ; true; <-ticker.C {
|
||||
log.Debug().Msg("Sending heartbeat")
|
||||
tlog.App.Debug().Msg("Sending heartbeat")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson))
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create heartbeat request")
|
||||
tlog.App.Error().Err(err).Msg("Failed to create heartbeat request")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -248,14 +247,14 @@ func (app *BootstrapApp) heartbeat() {
|
||||
res, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to send heartbeat")
|
||||
tlog.App.Error().Err(err).Msg("Failed to send heartbeat")
|
||||
continue
|
||||
}
|
||||
|
||||
res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 && res.StatusCode != 201 {
|
||||
log.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
|
||||
tlog.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,10 +265,10 @@ func (app *BootstrapApp) dbCleanup(queries *repository.Queries) {
|
||||
ctx := context.Background()
|
||||
|
||||
for ; true; <-ticker.C {
|
||||
log.Debug().Msg("Cleaning up old database sessions")
|
||||
tlog.App.Debug().Msg("Cleaning up old database sessions")
|
||||
err := queries.DeleteExpiredSessions(ctx, time.Now().Unix())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to clean up old database sessions")
|
||||
tlog.App.Error().Err(err).Msg("Failed to clean up old database sessions")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/middleware"
|
||||
@@ -15,7 +14,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||
engine.Use(gin.Recovery())
|
||||
|
||||
if len(app.config.Server.TrustedProxies) > 0 {
|
||||
err := engine.SetTrustedProxies(strings.Split(app.config.Server.TrustedProxies, ","))
|
||||
err := engine.SetTrustedProxies(app.config.Server.TrustedProxies)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set trusted proxies: %w", err)
|
||||
|
||||
@@ -3,8 +3,7 @@ package bootstrap
|
||||
import (
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
)
|
||||
|
||||
type Services struct {
|
||||
@@ -34,7 +33,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
||||
if err == nil {
|
||||
services.ldapService = ldapService
|
||||
} else {
|
||||
log.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without it")
|
||||
tlog.App.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without it")
|
||||
}
|
||||
|
||||
dockerService := service.NewDockerService()
|
||||
|
||||
@@ -16,13 +16,11 @@ var RedirectCookieName = "tinyauth-redirect"
|
||||
|
||||
type Config struct {
|
||||
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
||||
LogLevel string `description:"Log level (trace, debug, info, warn, error)." yaml:"logLevel"`
|
||||
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"`
|
||||
LogJSON bool `description:"Enable JSON formatted logs." yaml:"logJSON"`
|
||||
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
||||
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
||||
@@ -30,18 +28,19 @@ type Config struct {
|
||||
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"`
|
||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
||||
Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
||||
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
||||
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
||||
@@ -56,7 +55,7 @@ type IPConfig struct {
|
||||
}
|
||||
|
||||
type OAuthConfig struct {
|
||||
Whitelist string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
||||
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
||||
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
|
||||
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
||||
}
|
||||
@@ -78,6 +77,23 @@ type LdapConfig struct {
|
||||
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level"`
|
||||
Json bool `description:"Enable JSON formatted logs." yaml:"json"`
|
||||
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams"`
|
||||
}
|
||||
|
||||
type LogStreams struct {
|
||||
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http"`
|
||||
App LogStreamConfig `description:"Application logging." yaml:"app"`
|
||||
Audit LogStreamConfig `description:"Audit logging." yaml:"audit"`
|
||||
}
|
||||
|
||||
type LogStreamConfig struct {
|
||||
Enabled bool `description:"Enable this log stream." yaml:"enabled"`
|
||||
Level string `description:"Log level for this stream. Use global if empty." yaml:"level"`
|
||||
}
|
||||
|
||||
type ExperimentalConfig struct {
|
||||
ConfigFile string `description:"Path to config file." yaml:"-"`
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"net/url"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type UserContextResponse struct {
|
||||
@@ -61,7 +61,7 @@ type ContextController struct {
|
||||
|
||||
func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController {
|
||||
if config.DisableUIWarnings {
|
||||
log.Warn().Msg("UI warnings are disabled. This may expose users to security risks. Proceed with caution.")
|
||||
tlog.App.Warn().Msg("UI warnings are disabled. This may expose users to security risks. Proceed with caution.")
|
||||
}
|
||||
|
||||
return &ContextController{
|
||||
@@ -94,7 +94,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("No user context found in request")
|
||||
tlog.App.Debug().Err(err).Msg("No user context found in request")
|
||||
userContext.Status = 401
|
||||
userContext.Message = "Unauthorized"
|
||||
userContext.IsLoggedIn = false
|
||||
@@ -108,7 +108,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
||||
appUrl, err := url.Parse(controller.config.AppURL)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to parse app URL")
|
||||
tlog.App.Error().Err(err).Msg("Failed to parse app URL")
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "Internal Server Error",
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
@@ -48,6 +49,8 @@ var userContext = config.UserContext{
|
||||
}
|
||||
|
||||
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type OAuthRequest struct {
|
||||
@@ -54,7 +54,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||
|
||||
err := c.BindUri(&req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to bind URI")
|
||||
tlog.App.Error().Err(err).Msg("Failed to bind URI")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
@@ -65,7 +65,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||
service, exists := controller.broker.GetService(req.Provider)
|
||||
|
||||
if !exists {
|
||||
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
||||
tlog.App.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
||||
c.JSON(404, gin.H{
|
||||
"status": 404,
|
||||
"message": "Not Found",
|
||||
@@ -82,12 +82,12 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||
isRedirectSafe := utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain)
|
||||
|
||||
if !isRedirectSafe {
|
||||
log.Warn().Str("redirect_uri", redirectURI).Msg("Unsafe redirect URI detected, ignoring")
|
||||
tlog.App.Warn().Str("redirect_uri", redirectURI).Msg("Unsafe redirect URI detected, ignoring")
|
||||
redirectURI = ""
|
||||
}
|
||||
|
||||
if redirectURI != "" && isRedirectSafe {
|
||||
log.Debug().Msg("Setting redirect URI cookie")
|
||||
tlog.App.Debug().Msg("Setting redirect URI cookie")
|
||||
c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
err := c.BindUri(&req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to bind URI")
|
||||
tlog.App.Error().Err(err).Msg("Failed to bind URI")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
@@ -115,7 +115,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
|
||||
|
||||
if err != nil || state != csrfCookie {
|
||||
log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing")
|
||||
tlog.App.Warn().Err(err).Msg("CSRF token mismatch or cookie missing")
|
||||
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
@@ -127,14 +127,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
service, exists := controller.broker.GetService(req.Provider)
|
||||
|
||||
if !exists {
|
||||
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
||||
tlog.App.Warn().Msgf("OAuth provider not found: %s", req.Provider)
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
err = service.VerifyCode(code)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to verify OAuth code")
|
||||
tlog.App.Error().Err(err).Msg("Failed to verify OAuth code")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
@@ -142,26 +142,27 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
user, err := controller.broker.GetUser(req.Provider)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get user from OAuth provider")
|
||||
tlog.App.Error().Err(err).Msg("Failed to get user from OAuth provider")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if user.Email == "" {
|
||||
log.Error().Msg("OAuth provider did not return an email")
|
||||
tlog.App.Error().Msg("OAuth provider did not return an email")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
if !controller.auth.IsEmailWhitelisted(user.Email) {
|
||||
log.Warn().Str("email", user.Email).Msg("Email not whitelisted")
|
||||
tlog.App.Warn().Str("email", user.Email).Msg("Email not whitelisted")
|
||||
tlog.AuditLoginFailure(c, user.Email, req.Provider, "email not whitelisted")
|
||||
|
||||
queries, err := query.Values(config.UnauthorizedQuery{
|
||||
Username: user.Email,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
@@ -173,20 +174,20 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
var name string
|
||||
|
||||
if strings.TrimSpace(user.Name) != "" {
|
||||
log.Debug().Msg("Using name from OAuth provider")
|
||||
tlog.App.Debug().Msg("Using name from OAuth provider")
|
||||
name = user.Name
|
||||
} else {
|
||||
log.Debug().Msg("No name from OAuth provider, using pseudo name")
|
||||
tlog.App.Debug().Msg("No name from OAuth provider, using pseudo name")
|
||||
name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1])
|
||||
}
|
||||
|
||||
var username string
|
||||
|
||||
if strings.TrimSpace(user.PreferredUsername) != "" {
|
||||
log.Debug().Msg("Using preferred username from OAuth provider")
|
||||
tlog.App.Debug().Msg("Using preferred username from OAuth provider")
|
||||
username = user.PreferredUsername
|
||||
} else {
|
||||
log.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
|
||||
tlog.App.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
|
||||
username = strings.Replace(user.Email, "@", "_", -1)
|
||||
}
|
||||
|
||||
@@ -200,20 +201,22 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
OAuthSub: user.Sub,
|
||||
}
|
||||
|
||||
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||
|
||||
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create session cookie")
|
||||
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider)
|
||||
|
||||
redirectURI, err := c.Cookie(controller.config.RedirectCookieName)
|
||||
|
||||
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
|
||||
log.Debug().Msg("No redirect URI cookie found, redirecting to app root")
|
||||
tlog.App.Debug().Msg("No redirect URI cookie found, redirecting to app root")
|
||||
c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL)
|
||||
return
|
||||
}
|
||||
@@ -223,7 +226,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode redirect URI query")
|
||||
tlog.App.Error().Err(err).Msg("Failed to encode redirect URI query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var SupportedProxies = []string{"nginx", "traefik", "caddy", "envoy"}
|
||||
@@ -52,7 +52,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
|
||||
err := c.BindUri(&req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to bind URI")
|
||||
tlog.App.Error().Err(err).Msg("Failed to bind URI")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
@@ -61,7 +61,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if !slices.Contains(SupportedProxies, req.Proxy) {
|
||||
log.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
|
||||
tlog.App.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
@@ -73,7 +73,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
// Envoy uses the original client method for the external auth request
|
||||
// so we allow Any standard HTTP method for /api/auth/envoy
|
||||
if req.Proxy != "envoy" && c.Request.Method != http.MethodGet {
|
||||
log.Warn().Str("method", c.Request.Method).Msg("Invalid method for proxy")
|
||||
tlog.App.Warn().Str("method", c.Request.Method).Msg("Invalid method for proxy")
|
||||
c.Header("Allow", "GET")
|
||||
c.JSON(405, gin.H{
|
||||
"status": 405,
|
||||
@@ -85,9 +85,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
|
||||
|
||||
if isBrowser {
|
||||
log.Debug().Msg("Request identified as (most likely) coming from a browser")
|
||||
tlog.App.Debug().Msg("Request identified as (most likely) coming from a browser")
|
||||
} else {
|
||||
log.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
|
||||
tlog.App.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
|
||||
}
|
||||
|
||||
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
||||
@@ -98,12 +98,12 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
acls, err := controller.acls.GetAccessControls(host)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get access controls for resource")
|
||||
tlog.App.Error().Err(err).Msg("Failed to get access controls for resource")
|
||||
controller.handleError(c, req, isBrowser)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Interface("acls", acls).Msg("ACLs for resource")
|
||||
tlog.App.Trace().Interface("acls", acls).Msg("ACLs for resource")
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
@@ -119,13 +119,13 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
authEnabled, err := controller.auth.IsAuthEnabled(uri, acls.Path)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
|
||||
tlog.App.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
|
||||
controller.handleError(c, req, isBrowser)
|
||||
return
|
||||
}
|
||||
|
||||
if !authEnabled {
|
||||
log.Debug().Msg("Authentication disabled for resource, allowing access")
|
||||
tlog.App.Debug().Msg("Authentication disabled for resource, allowing access")
|
||||
controller.setHeaders(c, acls)
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
@@ -149,7 +149,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
context, err := utils.GetContext(c)
|
||||
|
||||
if err != nil {
|
||||
log.Debug().Msg("No user context found in request, treating as not logged in")
|
||||
tlog.App.Debug().Msg("No user context found in request, treating as not logged in")
|
||||
userContext = config.UserContext{
|
||||
IsLoggedIn: false,
|
||||
}
|
||||
@@ -171,10 +171,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
userContext = context
|
||||
}
|
||||
|
||||
log.Trace().Interface("context", userContext).Msg("User context from request")
|
||||
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
||||
|
||||
if userContext.Provider == "basic" && userContext.TotpEnabled {
|
||||
log.Debug().Msg("User has TOTP enabled, denying basic auth access")
|
||||
tlog.App.Debug().Msg("User has TOTP enabled, denying basic auth access")
|
||||
userContext.IsLoggedIn = false
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
|
||||
|
||||
if !userAllowed {
|
||||
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
|
||||
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
|
||||
|
||||
if req.Proxy == "nginx" || !isBrowser {
|
||||
c.JSON(403, gin.H{
|
||||
@@ -197,7 +197,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
@@ -216,7 +216,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
groupOK := controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
|
||||
|
||||
if !groupOK {
|
||||
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
|
||||
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
|
||||
|
||||
if req.Proxy == "nginx" || !isBrowser {
|
||||
c.JSON(403, gin.H{
|
||||
@@ -232,7 +232,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
@@ -276,7 +276,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode redirect URI query")
|
||||
tlog.App.Error().Err(err).Msg("Failed to encode redirect URI query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
@@ -290,14 +290,14 @@ func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) {
|
||||
headers := utils.ParseHeaders(acls.Response.Headers)
|
||||
|
||||
for key, value := range headers {
|
||||
log.Debug().Str("header", key).Msg("Setting header")
|
||||
tlog.App.Debug().Str("header", key).Msg("Setting header")
|
||||
c.Header(key, value)
|
||||
}
|
||||
|
||||
basicPassword := utils.GetSecret(acls.Response.BasicAuth.Password, acls.Response.BasicAuth.PasswordFile)
|
||||
|
||||
if acls.Response.BasicAuth.Username != "" && basicPassword != "" {
|
||||
log.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header")
|
||||
tlog.App.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header")
|
||||
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(acls.Response.BasicAuth.Username, basicPassword)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,15 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder, *service.AuthService) {
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
@@ -57,7 +60,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
|
||||
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
|
||||
},
|
||||
},
|
||||
OauthWhitelist: "",
|
||||
OauthWhitelist: []string{},
|
||||
SessionExpiry: 3600,
|
||||
SessionMaxLifetime: 0,
|
||||
SecureCookie: false,
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
@@ -53,7 +53,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to bind JSON")
|
||||
tlog.App.Error().Err(err).Msg("Failed to bind JSON")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
@@ -61,12 +61,13 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("username", req.Username).Msg("Login attempt")
|
||||
tlog.App.Debug().Str("username", req.Username).Msg("Login attempt")
|
||||
|
||||
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
|
||||
|
||||
if isLocked {
|
||||
log.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
|
||||
tlog.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
|
||||
tlog.AuditLoginFailure(c, req.Username, "username", "account locked")
|
||||
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||
c.JSON(429, gin.H{
|
||||
@@ -79,8 +80,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
userSearch := controller.auth.SearchUser(req.Username)
|
||||
|
||||
if userSearch.Type == "unknown" {
|
||||
log.Warn().Str("username", req.Username).Msg("User not found")
|
||||
tlog.App.Warn().Str("username", req.Username).Msg("User not found")
|
||||
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||
tlog.AuditLoginFailure(c, req.Username, "username", "user not found")
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -89,8 +91,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if !controller.auth.VerifyUser(userSearch, req.Password) {
|
||||
log.Warn().Str("username", req.Username).Msg("Invalid password")
|
||||
tlog.App.Warn().Str("username", req.Username).Msg("Invalid password")
|
||||
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||
tlog.AuditLoginFailure(c, req.Username, "username", "invalid password")
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -98,7 +101,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("username", req.Username).Msg("Login successful")
|
||||
tlog.App.Info().Str("username", req.Username).Msg("Login successful")
|
||||
tlog.AuditLoginSuccess(c, req.Username, "username")
|
||||
|
||||
controller.auth.RecordLoginAttempt(req.Username, true)
|
||||
|
||||
@@ -106,7 +110,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
user := controller.auth.GetLocalUser(userSearch.Username)
|
||||
|
||||
if user.TotpSecret != "" {
|
||||
log.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
||||
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
||||
|
||||
err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{
|
||||
Username: user.Username,
|
||||
@@ -117,7 +121,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create session cookie")
|
||||
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "Internal Server Error",
|
||||
@@ -141,12 +145,12 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
Provider: "username",
|
||||
}
|
||||
|
||||
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||
|
||||
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create session cookie")
|
||||
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "Internal Server Error",
|
||||
@@ -161,10 +165,15 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (controller *UserController) logoutHandler(c *gin.Context) {
|
||||
log.Debug().Msg("Logout request received")
|
||||
tlog.App.Debug().Msg("Logout request received")
|
||||
|
||||
controller.auth.DeleteSessionCookie(c)
|
||||
|
||||
context, err := utils.GetContext(c)
|
||||
if err == nil && context.IsLoggedIn {
|
||||
tlog.AuditLogout(c, context.Username, context.Provider)
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Logout successful",
|
||||
@@ -176,7 +185,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to bind JSON")
|
||||
tlog.App.Error().Err(err).Msg("Failed to bind JSON")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
@@ -187,7 +196,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
context, err := utils.GetContext(c)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get user context")
|
||||
tlog.App.Error().Err(err).Msg("Failed to get user context")
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "Internal Server Error",
|
||||
@@ -196,7 +205,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if !context.TotpPending {
|
||||
log.Warn().Msg("TOTP attempt without a pending TOTP session")
|
||||
tlog.App.Warn().Msg("TOTP attempt without a pending TOTP session")
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -204,12 +213,12 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("username", context.Username).Msg("TOTP verification attempt")
|
||||
tlog.App.Debug().Str("username", context.Username).Msg("TOTP verification attempt")
|
||||
|
||||
isLocked, remaining := controller.auth.IsAccountLocked(context.Username)
|
||||
|
||||
if isLocked {
|
||||
log.Warn().Str("username", context.Username).Msg("Account is locked due to too many failed TOTP attempts")
|
||||
tlog.App.Warn().Str("username", context.Username).Msg("Account is locked due to too many failed TOTP attempts")
|
||||
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||
c.JSON(429, gin.H{
|
||||
@@ -224,8 +233,9 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
ok := totp.Validate(req.Code, user.TotpSecret)
|
||||
|
||||
if !ok {
|
||||
log.Warn().Str("username", context.Username).Msg("Invalid TOTP code")
|
||||
tlog.App.Warn().Str("username", context.Username).Msg("Invalid TOTP code")
|
||||
controller.auth.RecordLoginAttempt(context.Username, false)
|
||||
tlog.AuditLoginFailure(c, context.Username, "totp", "invalid totp code")
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -233,7 +243,8 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("username", context.Username).Msg("TOTP verification successful")
|
||||
tlog.App.Info().Str("username", context.Username).Msg("TOTP verification successful")
|
||||
tlog.AuditLoginSuccess(c, context.Username, "totp")
|
||||
|
||||
controller.auth.RecordLoginAttempt(context.Username, true)
|
||||
|
||||
@@ -244,12 +255,12 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
Provider: "username",
|
||||
}
|
||||
|
||||
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||
|
||||
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create session cookie")
|
||||
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "Internal Server Error",
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
@@ -23,6 +24,8 @@ var cookieValue string
|
||||
var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ"
|
||||
|
||||
func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
@@ -60,7 +63,7 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng
|
||||
TotpSecret: totpSecret,
|
||||
},
|
||||
},
|
||||
OauthWhitelist: "",
|
||||
OauthWhitelist: []string{},
|
||||
SessionExpiry: 3600,
|
||||
SessionMaxLifetime: 0,
|
||||
SecureCookie: false,
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ContextMiddlewareConfig struct {
|
||||
@@ -40,7 +40,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
cookie, err := m.auth.GetSessionCookie(c)
|
||||
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("No valid session cookie found")
|
||||
tlog.App.Debug().Err(err).Msg("No valid session cookie found")
|
||||
goto basic
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
userSearch := m.auth.SearchUser(cookie.Username)
|
||||
|
||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
||||
log.Debug().Msg("User from session cookie not found")
|
||||
tlog.App.Debug().Msg("User from session cookie not found")
|
||||
m.auth.DeleteSessionCookie(c)
|
||||
goto basic
|
||||
}
|
||||
@@ -81,13 +81,13 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
_, exists := m.broker.GetService(cookie.Provider)
|
||||
|
||||
if !exists {
|
||||
log.Debug().Msg("OAuth provider from session cookie not found")
|
||||
tlog.App.Debug().Msg("OAuth provider from session cookie not found")
|
||||
m.auth.DeleteSessionCookie(c)
|
||||
goto basic
|
||||
}
|
||||
|
||||
if !m.auth.IsEmailWhitelisted(cookie.Email) {
|
||||
log.Debug().Msg("Email from session cookie not whitelisted")
|
||||
tlog.App.Debug().Msg("Email from session cookie not whitelisted")
|
||||
m.auth.DeleteSessionCookie(c)
|
||||
goto basic
|
||||
}
|
||||
@@ -112,7 +112,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
basic := m.auth.GetBasicAuth(c)
|
||||
|
||||
if basic == nil {
|
||||
log.Debug().Msg("No basic auth provided")
|
||||
tlog.App.Debug().Msg("No basic auth provided")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
locked, remaining := m.auth.IsAccountLocked(basic.Username)
|
||||
|
||||
if locked {
|
||||
log.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", basic.Username, remaining)
|
||||
tlog.App.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", basic.Username, remaining)
|
||||
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||
c.Next()
|
||||
@@ -131,14 +131,14 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
|
||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
||||
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||
log.Debug().Msg("User from basic auth not found")
|
||||
tlog.App.Debug().Msg("User from basic auth not found")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if !m.auth.VerifyUser(userSearch, basic.Password) {
|
||||
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||
log.Debug().Msg("Invalid password for basic auth user")
|
||||
tlog.App.Debug().Msg("Invalid password for basic auth user")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
|
||||
switch userSearch.Type {
|
||||
case "local":
|
||||
log.Debug().Msg("Basic auth user is local")
|
||||
tlog.App.Debug().Msg("Basic auth user is local")
|
||||
|
||||
user := m.auth.GetLocalUser(basic.Username)
|
||||
|
||||
@@ -162,7 +162,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
c.Next()
|
||||
return
|
||||
case "ldap":
|
||||
log.Debug().Msg("Basic auth user is LDAP")
|
||||
tlog.App.Debug().Msg("Basic auth user is LDAP")
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: basic.Username,
|
||||
Name: utils.Capitalize(basic.Username),
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,7 +49,7 @@ func (m *ZerologMiddleware) Middleware() gin.HandlerFunc {
|
||||
|
||||
latency := time.Since(tStart).String()
|
||||
|
||||
subLogger := log.With().Str("method", method).
|
||||
subLogger := tlog.HTTP.With().Str("method", method).
|
||||
Str("path", path).
|
||||
Str("address", address).
|
||||
Str("client_ip", clientIP).
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
)
|
||||
|
||||
type AccessControlsService struct {
|
||||
@@ -27,12 +27,12 @@ func (acls *AccessControlsService) Init() error {
|
||||
func (acls *AccessControlsService) lookupStaticACLs(domain string) (config.App, error) {
|
||||
for app, config := range acls.static {
|
||||
if config.Config.Domain == domain {
|
||||
log.Debug().Str("name", app).Msg("Found matching container by domain")
|
||||
tlog.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
||||
return config, nil
|
||||
}
|
||||
|
||||
if strings.SplitN(domain, ".", 2)[0] == app {
|
||||
log.Debug().Str("name", app).Msg("Found matching container by app name")
|
||||
tlog.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
||||
return config, nil
|
||||
}
|
||||
}
|
||||
@@ -44,11 +44,11 @@ func (acls *AccessControlsService) GetAccessControls(domain string) (config.App,
|
||||
app, err := acls.lookupStaticACLs(domain)
|
||||
|
||||
if err == nil {
|
||||
log.Debug().Msg("Using ACls from static configuration")
|
||||
tlog.App.Debug().Msg("Using ACls from static configuration")
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// Fallback to Docker labels
|
||||
log.Debug().Msg("Falling back to Docker labels for ACLs")
|
||||
tlog.App.Debug().Msg("Falling back to Docker labels for ACLs")
|
||||
return acls.docker.GetLabels(domain)
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ type LoginAttempt struct {
|
||||
|
||||
type AuthServiceConfig struct {
|
||||
Users []config.User
|
||||
OauthWhitelist string
|
||||
OauthWhitelist []string
|
||||
SessionExpiry int
|
||||
SessionMaxLifetime int
|
||||
SecureCookie bool
|
||||
@@ -73,7 +73,7 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch {
|
||||
userDN, err := auth.ldap.Search(username)
|
||||
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
|
||||
tlog.App.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
|
||||
return config.UserSearch{
|
||||
Type: "error",
|
||||
}
|
||||
@@ -99,24 +99,24 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b
|
||||
if auth.ldap != nil {
|
||||
err := auth.ldap.Bind(search.Username, password)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
|
||||
tlog.App.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
|
||||
return false
|
||||
}
|
||||
|
||||
err = auth.ldap.BindService(true)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
|
||||
tlog.App.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
default:
|
||||
log.Debug().Str("type", search.Type).Msg("Unknown user type for authentication")
|
||||
tlog.App.Debug().Str("type", search.Type).Msg("Unknown user type for authentication")
|
||||
return false
|
||||
}
|
||||
|
||||
log.Warn().Str("username", search.Username).Msg("User authentication failed")
|
||||
tlog.App.Warn().Str("username", search.Username).Msg("User authentication failed")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func (auth *AuthService) GetLocalUser(username string) config.User {
|
||||
}
|
||||
}
|
||||
|
||||
log.Warn().Str("username", username).Msg("Local user not found")
|
||||
tlog.App.Warn().Str("username", username).Msg("Local user not found")
|
||||
return config.User{}
|
||||
}
|
||||
|
||||
@@ -182,12 +182,12 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
|
||||
|
||||
if attempt.FailedAttempts >= auth.config.LoginMaxRetries {
|
||||
attempt.LockedUntil = time.Now().Add(time.Duration(auth.config.LoginTimeout) * time.Second)
|
||||
log.Warn().Str("identifier", identifier).Int("timeout", auth.config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
|
||||
tlog.App.Warn().Str("identifier", identifier).Int("timeout", auth.config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
||||
return utils.CheckFilter(auth.config.OauthWhitelist, email)
|
||||
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
||||
}
|
||||
|
||||
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error {
|
||||
@@ -277,7 +277,7 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
||||
}
|
||||
|
||||
c.SetCookie(auth.config.SessionCookieName, cookie, int(newExpiry-currentTime), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
||||
log.Trace().Str("username", session.Username).Msg("Session cookie refreshed")
|
||||
tlog.App.Trace().Str("username", session.Username).Msg("Session cookie refreshed")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -322,7 +322,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
||||
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
|
||||
err = auth.queries.DeleteSession(c, cookie)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
|
||||
tlog.App.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
|
||||
}
|
||||
return config.SessionCookie{}, fmt.Errorf("session expired due to max lifetime exceeded")
|
||||
}
|
||||
@@ -331,7 +331,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
||||
if currentTime > session.Expiry {
|
||||
err = auth.queries.DeleteSession(c, cookie)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to delete expired session")
|
||||
tlog.App.Error().Err(err).Msg("Failed to delete expired session")
|
||||
}
|
||||
return config.SessionCookie{}, fmt.Errorf("session expired")
|
||||
}
|
||||
@@ -355,18 +355,18 @@ func (auth *AuthService) UserAuthConfigured() bool {
|
||||
|
||||
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
||||
if context.OAuth {
|
||||
log.Debug().Msg("Checking OAuth whitelist")
|
||||
tlog.App.Debug().Msg("Checking OAuth whitelist")
|
||||
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
|
||||
}
|
||||
|
||||
if acls.Users.Block != "" {
|
||||
log.Debug().Msg("Checking blocked users")
|
||||
tlog.App.Debug().Msg("Checking blocked users")
|
||||
if utils.CheckFilter(acls.Users.Block, context.Username) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msg("Checking users")
|
||||
tlog.App.Debug().Msg("Checking users")
|
||||
return utils.CheckFilter(acls.Users.Allow, context.Username)
|
||||
}
|
||||
|
||||
@@ -377,19 +377,19 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte
|
||||
|
||||
for id := range config.OverrideProviders {
|
||||
if context.Provider == id {
|
||||
log.Info().Str("provider", id).Msg("OAuth groups not supported for this provider")
|
||||
tlog.App.Info().Str("provider", id).Msg("OAuth groups not supported for this provider")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
|
||||
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
||||
log.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msg("No groups matched")
|
||||
tlog.App.Debug().Msg("No groups matched")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@ func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, e
|
||||
func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
|
||||
username, password, ok := c.Request.BasicAuth()
|
||||
if !ok {
|
||||
log.Debug().Msg("No basic auth provided")
|
||||
tlog.App.Debug().Msg("No basic auth provided")
|
||||
return nil
|
||||
}
|
||||
return &config.User{
|
||||
@@ -443,11 +443,11 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
||||
for _, blocked := range blockedIps {
|
||||
res, err := utils.FilterIP(blocked, ip)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
||||
tlog.App.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
||||
continue
|
||||
}
|
||||
if res {
|
||||
log.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access")
|
||||
tlog.App.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access")
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -455,21 +455,21 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
||||
for _, allowed := range allowedIPs {
|
||||
res, err := utils.FilterIP(allowed, ip)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
||||
tlog.App.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
||||
continue
|
||||
}
|
||||
if res {
|
||||
log.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access")
|
||||
tlog.App.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if len(allowedIPs) > 0 {
|
||||
log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
||||
tlog.App.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
||||
return false
|
||||
}
|
||||
|
||||
log.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default")
|
||||
tlog.App.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -477,15 +477,15 @@ func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
|
||||
for _, bypassed := range acls.Bypass {
|
||||
res, err := utils.FilterIP(bypassed, ip)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
||||
tlog.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
||||
continue
|
||||
}
|
||||
if res {
|
||||
log.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, allowing access")
|
||||
tlog.App.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, allowing access")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Str("ip", ip).Msg("IP not in bypass list, continuing with authentication")
|
||||
tlog.App.Debug().Str("ip", ip).Msg("IP not in bypass list, continuing with authentication")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/decoders"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
container "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type DockerService struct {
|
||||
@@ -37,7 +37,7 @@ func (docker *DockerService) Init() error {
|
||||
_, err = docker.client.Ping(docker.context)
|
||||
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("Docker not connected")
|
||||
tlog.App.Debug().Err(err).Msg("Docker not connected")
|
||||
docker.isConnected = false
|
||||
docker.client = nil
|
||||
docker.context = nil
|
||||
@@ -45,7 +45,7 @@ func (docker *DockerService) Init() error {
|
||||
}
|
||||
|
||||
docker.isConnected = true
|
||||
log.Debug().Msg("Docker connected")
|
||||
tlog.App.Debug().Msg("Docker connected")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func (docker *DockerService) inspectContainer(containerId string) (container.Ins
|
||||
|
||||
func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
|
||||
if !docker.isConnected {
|
||||
log.Debug().Msg("Docker not connected, returning empty labels")
|
||||
tlog.App.Debug().Msg("Docker not connected, returning empty labels")
|
||||
return config.App{}, nil
|
||||
}
|
||||
|
||||
@@ -90,17 +90,17 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
|
||||
|
||||
for appName, appLabels := range labels.Apps {
|
||||
if appLabels.Config.Domain == appDomain {
|
||||
log.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
|
||||
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
|
||||
return appLabels, nil
|
||||
}
|
||||
|
||||
if strings.SplitN(appDomain, ".", 2)[0] == appName {
|
||||
log.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
|
||||
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
|
||||
return appLabels, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msg("No matching container found, returning empty labels")
|
||||
tlog.App.Debug().Msg("No matching container found, returning empty labels")
|
||||
return config.App{}, nil
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -117,7 +117,7 @@ func (generic *GenericOAuthService) Userinfo() (config.Claims, error) {
|
||||
return user, err
|
||||
}
|
||||
|
||||
log.Trace().Str("body", string(body)).Msg("Userinfo response body")
|
||||
tlog.App.Trace().Str("body", string(body)).Msg("Userinfo response body")
|
||||
|
||||
err = json.Unmarshal(body, &user)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
ldapgo "github.com/go-ldap/ldap/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
)
|
||||
|
||||
type LdapServiceConfig struct {
|
||||
@@ -44,7 +44,7 @@ func (ldap *LdapService) Init() error {
|
||||
return fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err)
|
||||
}
|
||||
ldap.cert = &cert
|
||||
log.Info().Msg("Using LDAP with mTLS authentication")
|
||||
tlog.App.Info().Msg("Using LDAP with mTLS authentication")
|
||||
|
||||
// TODO: Add optional extra CA certificates, instead of `InsecureSkipVerify`
|
||||
/*
|
||||
@@ -66,12 +66,12 @@ func (ldap *LdapService) Init() error {
|
||||
for range time.Tick(time.Duration(5) * time.Minute) {
|
||||
err := ldap.heartbeat()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("LDAP connection heartbeat failed")
|
||||
tlog.App.Error().Err(err).Msg("LDAP connection heartbeat failed")
|
||||
if reconnectErr := ldap.reconnect(); reconnectErr != nil {
|
||||
log.Error().Err(reconnectErr).Msg("Failed to reconnect to LDAP server")
|
||||
tlog.App.Error().Err(reconnectErr).Msg("Failed to reconnect to LDAP server")
|
||||
continue
|
||||
}
|
||||
log.Info().Msg("Successfully reconnected to LDAP server")
|
||||
tlog.App.Info().Msg("Successfully reconnected to LDAP server")
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -169,7 +169,7 @@ func (ldap *LdapService) Bind(userDN string, password string) error {
|
||||
}
|
||||
|
||||
func (ldap *LdapService) heartbeat() error {
|
||||
log.Debug().Msg("Performing LDAP connection heartbeat")
|
||||
tlog.App.Debug().Msg("Performing LDAP connection heartbeat")
|
||||
|
||||
searchRequest := ldapgo.NewSearchRequest(
|
||||
"",
|
||||
@@ -191,7 +191,7 @@ func (ldap *LdapService) heartbeat() error {
|
||||
}
|
||||
|
||||
func (ldap *LdapService) reconnect() error {
|
||||
log.Info().Msg("Reconnecting to LDAP server")
|
||||
tlog.App.Info().Msg("Reconnecting to LDAP server")
|
||||
|
||||
exp := backoff.NewExponentialBackOff()
|
||||
exp.InitialInterval = 500 * time.Millisecond
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
@@ -49,10 +49,10 @@ func (broker *OAuthBrokerService) Init() error {
|
||||
for name, service := range broker.services {
|
||||
err := service.Init()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to initialize OAuth service: %T", name)
|
||||
tlog.App.Error().Err(err).Msgf("Failed to initialize OAuth service: %s", name)
|
||||
return err
|
||||
}
|
||||
log.Info().Str("service", name).Msg("Initialized OAuth service")
|
||||
tlog.App.Info().Str("service", name).Msg("Initialized OAuth service")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
39
internal/utils/tlog/log_audit.go
Normal file
39
internal/utils/tlog/log_audit.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package tlog
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// functions here use CallerSkipFrame to ensure correct caller info is logged
|
||||
|
||||
func AuditLoginSuccess(c *gin.Context, username, provider string) {
|
||||
Audit.Info().
|
||||
CallerSkipFrame(1).
|
||||
Str("event", "login").
|
||||
Str("result", "success").
|
||||
Str("username", username).
|
||||
Str("provider", provider).
|
||||
Str("ip", c.ClientIP()).
|
||||
Send()
|
||||
}
|
||||
|
||||
func AuditLoginFailure(c *gin.Context, username, provider string, reason string) {
|
||||
Audit.Warn().
|
||||
CallerSkipFrame(1).
|
||||
Str("event", "login").
|
||||
Str("result", "failure").
|
||||
Str("username", username).
|
||||
Str("provider", provider).
|
||||
Str("ip", c.ClientIP()).
|
||||
Str("reason", reason).
|
||||
Send()
|
||||
}
|
||||
|
||||
func AuditLogout(c *gin.Context, username, provider string) {
|
||||
Audit.Info().
|
||||
CallerSkipFrame(1).
|
||||
Str("event", "logout").
|
||||
Str("result", "success").
|
||||
Str("username", username).
|
||||
Str("provider", provider).
|
||||
Str("ip", c.ClientIP()).
|
||||
Send()
|
||||
}
|
||||
86
internal/utils/tlog/log_wrapper.go
Normal file
86
internal/utils/tlog/log_wrapper.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package tlog
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
Audit zerolog.Logger
|
||||
HTTP zerolog.Logger
|
||||
App zerolog.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
Audit zerolog.Logger
|
||||
HTTP zerolog.Logger
|
||||
App zerolog.Logger
|
||||
)
|
||||
|
||||
func NewLogger(cfg config.LogConfig) *Logger {
|
||||
baseLogger := log.With().
|
||||
Timestamp().
|
||||
Caller().
|
||||
Logger().
|
||||
Level(parseLogLevel(cfg.Level))
|
||||
|
||||
if !cfg.Json {
|
||||
baseLogger = baseLogger.Output(zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: time.RFC3339,
|
||||
})
|
||||
}
|
||||
|
||||
return &Logger{
|
||||
Audit: createLogger("audit", cfg.Streams.Audit, baseLogger),
|
||||
HTTP: createLogger("http", cfg.Streams.HTTP, baseLogger),
|
||||
App: createLogger("app", cfg.Streams.App, baseLogger),
|
||||
}
|
||||
}
|
||||
|
||||
func NewSimpleLogger() *Logger {
|
||||
return NewLogger(config.LogConfig{
|
||||
Level: "info",
|
||||
Json: false,
|
||||
Streams: config.LogStreams{
|
||||
HTTP: config.LogStreamConfig{Enabled: true},
|
||||
App: config.LogStreamConfig{Enabled: true},
|
||||
Audit: config.LogStreamConfig{Enabled: false},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Logger) Init() {
|
||||
Audit = l.Audit
|
||||
HTTP = l.HTTP
|
||||
App = l.App
|
||||
}
|
||||
|
||||
func createLogger(component string, streamCfg config.LogStreamConfig, baseLogger zerolog.Logger) zerolog.Logger {
|
||||
if !streamCfg.Enabled {
|
||||
return zerolog.Nop()
|
||||
}
|
||||
subLogger := baseLogger.With().Str("log_stream", component).Logger()
|
||||
// override level if specified, otherwise use base level
|
||||
if streamCfg.Level != "" {
|
||||
subLogger = subLogger.Level(parseLogLevel(streamCfg.Level))
|
||||
}
|
||||
return subLogger
|
||||
}
|
||||
|
||||
func parseLogLevel(level string) zerolog.Level {
|
||||
if level == "" {
|
||||
return zerolog.InfoLevel
|
||||
}
|
||||
parsedLevel, err := zerolog.ParseLevel(strings.ToLower(level))
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("level", level).Msg("Invalid log level, defaulting to info")
|
||||
parsedLevel = zerolog.InfoLevel
|
||||
}
|
||||
return parsedLevel
|
||||
}
|
||||
93
internal/utils/tlog/log_wrapper_test.go
Normal file
93
internal/utils/tlog/log_wrapper_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package tlog_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestNewLogger(t *testing.T) {
|
||||
cfg := config.LogConfig{
|
||||
Level: "debug",
|
||||
Json: true,
|
||||
Streams: config.LogStreams{
|
||||
HTTP: config.LogStreamConfig{Enabled: true, Level: "info"},
|
||||
App: config.LogStreamConfig{Enabled: true, Level: ""},
|
||||
Audit: config.LogStreamConfig{Enabled: false, Level: ""},
|
||||
},
|
||||
}
|
||||
|
||||
logger := tlog.NewLogger(cfg)
|
||||
|
||||
assert.Assert(t, logger != nil)
|
||||
assert.Assert(t, logger.HTTP.GetLevel() == zerolog.InfoLevel)
|
||||
assert.Assert(t, logger.App.GetLevel() == zerolog.DebugLevel)
|
||||
assert.Assert(t, logger.Audit.GetLevel() == zerolog.Disabled)
|
||||
}
|
||||
|
||||
func TestNewSimpleLogger(t *testing.T) {
|
||||
logger := tlog.NewSimpleLogger()
|
||||
assert.Assert(t, logger != nil)
|
||||
assert.Assert(t, logger.HTTP.GetLevel() == zerolog.InfoLevel)
|
||||
assert.Assert(t, logger.App.GetLevel() == zerolog.InfoLevel)
|
||||
assert.Assert(t, logger.Audit.GetLevel() == zerolog.Disabled)
|
||||
}
|
||||
|
||||
func TestLoggerInit(t *testing.T) {
|
||||
logger := tlog.NewSimpleLogger()
|
||||
logger.Init()
|
||||
|
||||
assert.Assert(t, tlog.App.GetLevel() != zerolog.Disabled)
|
||||
}
|
||||
|
||||
func TestLoggerWithDisabledStreams(t *testing.T) {
|
||||
cfg := config.LogConfig{
|
||||
Level: "info",
|
||||
Json: false,
|
||||
Streams: config.LogStreams{
|
||||
HTTP: config.LogStreamConfig{Enabled: false},
|
||||
App: config.LogStreamConfig{Enabled: false},
|
||||
Audit: config.LogStreamConfig{Enabled: false},
|
||||
},
|
||||
}
|
||||
|
||||
logger := tlog.NewLogger(cfg)
|
||||
|
||||
assert.Assert(t, logger.HTTP.GetLevel() == zerolog.Disabled)
|
||||
assert.Assert(t, logger.App.GetLevel() == zerolog.Disabled)
|
||||
assert.Assert(t, logger.Audit.GetLevel() == zerolog.Disabled)
|
||||
}
|
||||
|
||||
func TestLogStreamField(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
cfg := config.LogConfig{
|
||||
Level: "info",
|
||||
Json: true,
|
||||
Streams: config.LogStreams{
|
||||
HTTP: config.LogStreamConfig{Enabled: true},
|
||||
App: config.LogStreamConfig{Enabled: true},
|
||||
Audit: config.LogStreamConfig{Enabled: true},
|
||||
},
|
||||
}
|
||||
|
||||
logger := tlog.NewLogger(cfg)
|
||||
|
||||
// Override output for HTTP logger to capture output
|
||||
logger.HTTP = logger.HTTP.Output(&buf)
|
||||
|
||||
logger.HTTP.Info().Msg("test message")
|
||||
|
||||
var logEntry map[string]interface{}
|
||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, "http", logEntry["log_stream"])
|
||||
assert.Equal(t, "test message", logEntry["message"])
|
||||
}
|
||||
@@ -7,22 +7,14 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
)
|
||||
|
||||
func ParseUsers(users string) ([]config.User, error) {
|
||||
var usersParsed []config.User
|
||||
func ParseUsers(usersStr []string) ([]config.User, error) {
|
||||
var users []config.User
|
||||
|
||||
users = strings.TrimSpace(users)
|
||||
|
||||
if users == "" {
|
||||
if len(usersStr) == 0 {
|
||||
return []config.User{}, nil
|
||||
}
|
||||
|
||||
userList := strings.Split(users, ",")
|
||||
|
||||
if len(userList) == 0 {
|
||||
return []config.User{}, errors.New("invalid user format")
|
||||
}
|
||||
|
||||
for _, user := range userList {
|
||||
for _, user := range usersStr {
|
||||
if strings.TrimSpace(user) == "" {
|
||||
continue
|
||||
}
|
||||
@@ -30,64 +22,71 @@ func ParseUsers(users string) ([]config.User, error) {
|
||||
if err != nil {
|
||||
return []config.User{}, err
|
||||
}
|
||||
usersParsed = append(usersParsed, parsed)
|
||||
users = append(users, parsed)
|
||||
}
|
||||
|
||||
return usersParsed, nil
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetUsers(conf string, file string) ([]config.User, error) {
|
||||
var users string
|
||||
func GetUsers(usersCfg []string, usersPath string) ([]config.User, error) {
|
||||
var usersStr []string
|
||||
|
||||
if conf == "" && file == "" {
|
||||
if len(usersCfg) == 0 && usersPath == "" {
|
||||
return []config.User{}, nil
|
||||
}
|
||||
|
||||
if conf != "" {
|
||||
users += conf
|
||||
if len(usersCfg) > 0 {
|
||||
usersStr = append(usersStr, usersCfg...)
|
||||
}
|
||||
|
||||
if file != "" {
|
||||
contents, err := ReadFile(file)
|
||||
if usersPath != "" {
|
||||
contents, err := ReadFile(usersPath)
|
||||
|
||||
if err != nil {
|
||||
return []config.User{}, err
|
||||
}
|
||||
if users != "" {
|
||||
users += ","
|
||||
|
||||
lines := strings.SplitSeq(contents, "\n")
|
||||
|
||||
for line := range lines {
|
||||
lineTrimmed := strings.TrimSpace(line)
|
||||
if lineTrimmed == "" {
|
||||
continue
|
||||
}
|
||||
usersStr = append(usersStr, lineTrimmed)
|
||||
}
|
||||
users += ParseFileToLine(contents)
|
||||
}
|
||||
|
||||
return ParseUsers(users)
|
||||
return ParseUsers(usersStr)
|
||||
}
|
||||
|
||||
func ParseUser(user string) (config.User, error) {
|
||||
if strings.Contains(user, "$$") {
|
||||
user = strings.ReplaceAll(user, "$$", "$")
|
||||
func ParseUser(userStr string) (config.User, error) {
|
||||
if strings.Contains(userStr, "$$") {
|
||||
userStr = strings.ReplaceAll(userStr, "$$", "$")
|
||||
}
|
||||
|
||||
userSplit := strings.Split(user, ":")
|
||||
parts := strings.SplitN(userStr, ":", 4)
|
||||
|
||||
if len(userSplit) < 2 || len(userSplit) > 3 {
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
return config.User{}, errors.New("invalid user format")
|
||||
}
|
||||
|
||||
for _, userPart := range userSplit {
|
||||
if strings.TrimSpace(userPart) == "" {
|
||||
for i, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
return config.User{}, errors.New("invalid user format")
|
||||
}
|
||||
parts[i] = trimmed
|
||||
}
|
||||
|
||||
if len(userSplit) == 2 {
|
||||
return config.User{
|
||||
Username: strings.TrimSpace(userSplit[0]),
|
||||
Password: strings.TrimSpace(userSplit[1]),
|
||||
}, nil
|
||||
user := config.User{
|
||||
Username: parts[0],
|
||||
Password: parts[1],
|
||||
}
|
||||
|
||||
return config.User{
|
||||
Username: strings.TrimSpace(userSplit[0]),
|
||||
Password: strings.TrimSpace(userSplit[1]),
|
||||
TotpSecret: strings.TrimSpace(userSplit[2]),
|
||||
}, nil
|
||||
if len(parts) == 3 {
|
||||
user.TotpSecret = parts[2]
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestGetUsers(t *testing.T) {
|
||||
defer os.Remove("/tmp/tinyauth_users_test.txt")
|
||||
|
||||
// Test file
|
||||
users, err := utils.GetUsers("", "/tmp/tinyauth_users_test.txt")
|
||||
users, err := utils.GetUsers([]string{}, "/tmp/tinyauth_users_test.txt")
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestGetUsers(t *testing.T) {
|
||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
|
||||
|
||||
// Test config
|
||||
users, err = utils.GetUsers("user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "")
|
||||
users, err = utils.GetUsers([]string{"user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "")
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestGetUsers(t *testing.T) {
|
||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
|
||||
|
||||
// Test both
|
||||
users, err = utils.GetUsers("user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "/tmp/tinyauth_users_test.txt")
|
||||
users, err = utils.GetUsers([]string{"user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "/tmp/tinyauth_users_test.txt")
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -60,14 +60,14 @@ func TestGetUsers(t *testing.T) {
|
||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[2].Password)
|
||||
|
||||
// Test empty
|
||||
users, err = utils.GetUsers("", "")
|
||||
users, err = utils.GetUsers([]string{}, "")
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, 0, len(users))
|
||||
|
||||
// Test non-existent file
|
||||
users, err = utils.GetUsers("", "/tmp/non_existent_file.txt")
|
||||
users, err = utils.GetUsers([]string{}, "/tmp/non_existent_file.txt")
|
||||
|
||||
assert.ErrorContains(t, err, "no such file or directory")
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestGetUsers(t *testing.T) {
|
||||
|
||||
func TestParseUsers(t *testing.T) {
|
||||
// Valid users
|
||||
users, err := utils.ParseUsers("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF") // user2 has TOTP
|
||||
users, err := utils.ParseUsers([]string{"user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF"}) // user2 has TOTP
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestParseUsers(t *testing.T) {
|
||||
assert.Equal(t, "ABCDEF", users[1].TotpSecret)
|
||||
|
||||
// Valid weirdly spaced users
|
||||
users, err = utils.ParseUsers(" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G , user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF ") // Spacing is on purpose
|
||||
users, err = utils.ParseUsers([]string{" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G ", " user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF "}) // Spacing is on purpose
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(users))
|
||||
|
||||
Reference in New Issue
Block a user