mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-28 20:55:42 +00:00
Compare commits
29 Commits
v1.0.0
...
v2.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c4f086008 | ||
|
|
6e5f882e0b | ||
|
|
99268f80c9 | ||
|
|
dcd816b6c6 | ||
|
|
381f6ef76f | ||
|
|
8a8ba18ded | ||
|
|
29f0a94faf | ||
|
|
6602e8140b | ||
|
|
2385599c80 | ||
|
|
6f184856f1 | ||
|
|
e2e3b3bdc6 | ||
|
|
3efcb26db1 | ||
|
|
c54267f50d | ||
|
|
4de12ce5c1 | ||
|
|
0cf0aafc14 | ||
|
|
80ea43184c | ||
|
|
3c4dffd479 | ||
|
|
f19f40f9fc | ||
|
|
a243f22ac8 | ||
|
|
08d382c981 | ||
|
|
94f7debb10 | ||
|
|
3b50d9303b | ||
|
|
d67133aca7 | ||
|
|
989ea8f229 | ||
|
|
708006decf | ||
|
|
682a918812 | ||
|
|
389248cfe1 | ||
|
|
81d25061df | ||
|
|
f59697955d |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -9,3 +9,7 @@ docker-compose.test.yml
|
|||||||
|
|
||||||
# users file
|
# users file
|
||||||
users.txt
|
users.txt
|
||||||
|
|
||||||
|
# secret test file
|
||||||
|
secret.txt
|
||||||
|
secret_oauth.txt
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
Tinyauth is a simple authentication middleware that adds simple email/password login to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
|
Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
||||||
|
|||||||
92
cmd/root.go
92
cmd/root.go
@@ -1,15 +1,21 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
cmd "tinyauth/cmd/user"
|
cmd "tinyauth/cmd/user"
|
||||||
"tinyauth/internal/api"
|
"tinyauth/internal/api"
|
||||||
|
"tinyauth/internal/assets"
|
||||||
"tinyauth/internal/auth"
|
"tinyauth/internal/auth"
|
||||||
|
"tinyauth/internal/docker"
|
||||||
"tinyauth/internal/hooks"
|
"tinyauth/internal/hooks"
|
||||||
"tinyauth/internal/providers"
|
"tinyauth/internal/providers"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
"tinyauth/internal/utils"
|
"tinyauth/internal/utils"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@@ -17,47 +23,44 @@ import (
|
|||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "tinyauth",
|
Use: "tinyauth",
|
||||||
Short: "An extremely simple traefik forward auth proxy.",
|
Short: "The simplest way to protect your apps with a login screen.",
|
||||||
Long: `Tinyauth is an extremely simple traefik forward-auth login screen that makes securing your apps easy.`,
|
Long: `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// Logger
|
||||||
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.FatalLevel)
|
||||||
|
|
||||||
// Get config
|
// Get config
|
||||||
log.Info().Msg("Parsing config")
|
|
||||||
var config types.Config
|
var config types.Config
|
||||||
parseErr := viper.Unmarshal(&config)
|
parseErr := viper.Unmarshal(&config)
|
||||||
HandleError(parseErr, "Failed to parse config")
|
HandleError(parseErr, "Failed to parse config")
|
||||||
|
|
||||||
|
// Secrets
|
||||||
|
config.Secret = utils.GetSecret(config.Secret, config.SecretFile)
|
||||||
|
config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
|
||||||
|
config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
|
||||||
|
config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
|
||||||
|
config.TailscaleClientSecret = utils.GetSecret(config.TailscaleClientSecret, config.TailscaleClientSecretFile)
|
||||||
|
|
||||||
// Validate config
|
// Validate config
|
||||||
log.Info().Msg("Validating config")
|
|
||||||
validator := validator.New()
|
validator := validator.New()
|
||||||
validateErr := validator.Struct(config)
|
validateErr := validator.Struct(config)
|
||||||
HandleError(validateErr, "Invalid config")
|
HandleError(validateErr, "Failed to validate config")
|
||||||
|
|
||||||
// Parse users
|
// Logger
|
||||||
|
log.Logger = log.Level(zerolog.Level(config.LogLevel))
|
||||||
|
log.Info().Str("version", assets.Version).Msg("Starting tinyauth")
|
||||||
|
|
||||||
|
// Users
|
||||||
log.Info().Msg("Parsing users")
|
log.Info().Msg("Parsing users")
|
||||||
|
users, usersErr := utils.GetUsers(config.Users, config.UsersFile)
|
||||||
|
|
||||||
if config.UsersFile == "" && config.Users == "" {
|
if (len(users) == 0 || usersErr != nil) && !utils.OAuthConfigured(config) {
|
||||||
log.Fatal().Msg("No users provided")
|
log.Fatal().Err(usersErr).Msg("Failed to parse users")
|
||||||
}
|
}
|
||||||
|
|
||||||
usersString := config.Users
|
|
||||||
|
|
||||||
if config.UsersFile != "" {
|
|
||||||
log.Info().Msg("Reading users from file")
|
|
||||||
usersFromFile, readErr := utils.GetUsersFromFile(config.UsersFile)
|
|
||||||
HandleError(readErr, "Failed to read users from file")
|
|
||||||
usersFromFileParsed := utils.ParseFileToLine(usersFromFile)
|
|
||||||
if usersString != "" {
|
|
||||||
usersString = usersString + "," + usersFromFileParsed
|
|
||||||
} else {
|
|
||||||
usersString = usersFromFileParsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
users, parseErr := utils.ParseUsers(usersString)
|
|
||||||
HandleError(parseErr, "Failed to parse users")
|
|
||||||
|
|
||||||
// Create oauth whitelist
|
// Create oauth whitelist
|
||||||
oauthWhitelist := utils.ParseCommaString(config.OAuthWhitelist)
|
oauthWhitelist := strings.Split(config.OAuthWhitelist, ",")
|
||||||
|
log.Debug().Msg("Parsed OAuth whitelist")
|
||||||
|
|
||||||
// Create OAuth config
|
// Create OAuth config
|
||||||
oauthConfig := types.OAuthConfig{
|
oauthConfig := types.OAuthConfig{
|
||||||
@@ -65,17 +68,28 @@ var rootCmd = &cobra.Command{
|
|||||||
GithubClientSecret: config.GithubClientSecret,
|
GithubClientSecret: config.GithubClientSecret,
|
||||||
GoogleClientId: config.GoogleClientId,
|
GoogleClientId: config.GoogleClientId,
|
||||||
GoogleClientSecret: config.GoogleClientSecret,
|
GoogleClientSecret: config.GoogleClientSecret,
|
||||||
|
TailscaleClientId: config.TailscaleClientId,
|
||||||
|
TailscaleClientSecret: config.TailscaleClientSecret,
|
||||||
GenericClientId: config.GenericClientId,
|
GenericClientId: config.GenericClientId,
|
||||||
GenericClientSecret: config.GenericClientSecret,
|
GenericClientSecret: config.GenericClientSecret,
|
||||||
GenericScopes: utils.ParseCommaString(config.GenericScopes),
|
GenericScopes: strings.Split(config.GenericScopes, ","),
|
||||||
GenericAuthURL: config.GenericAuthURL,
|
GenericAuthURL: config.GenericAuthURL,
|
||||||
GenericTokenURL: config.GenericTokenURL,
|
GenericTokenURL: config.GenericTokenURL,
|
||||||
GenericUserURL: config.GenericUserURL,
|
GenericUserURL: config.GenericUserURL,
|
||||||
AppURL: config.AppURL,
|
AppURL: config.AppURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Parsed OAuth config")
|
||||||
|
|
||||||
|
// Create docker service
|
||||||
|
docker := docker.NewDocker()
|
||||||
|
|
||||||
|
// Initialize docker
|
||||||
|
dockerErr := docker.Init()
|
||||||
|
HandleError(dockerErr, "Failed to initialize docker")
|
||||||
|
|
||||||
// Create auth service
|
// Create auth service
|
||||||
auth := auth.NewAuth(users, oauthWhitelist)
|
auth := auth.NewAuth(docker, users, oauthWhitelist)
|
||||||
|
|
||||||
// Create OAuth providers service
|
// Create OAuth providers service
|
||||||
providers := providers.NewProviders(oauthConfig)
|
providers := providers.NewProviders(oauthConfig)
|
||||||
@@ -125,16 +139,23 @@ func init() {
|
|||||||
rootCmd.Flags().Int("port", 3000, "Port to run the server on.")
|
rootCmd.Flags().Int("port", 3000, "Port to run the server on.")
|
||||||
rootCmd.Flags().String("address", "0.0.0.0", "Address to bind the server to.")
|
rootCmd.Flags().String("address", "0.0.0.0", "Address to bind the server to.")
|
||||||
rootCmd.Flags().String("secret", "", "Secret to use for the cookie.")
|
rootCmd.Flags().String("secret", "", "Secret to use for the cookie.")
|
||||||
|
rootCmd.Flags().String("secret-file", "", "Path to a file containing the secret.")
|
||||||
rootCmd.Flags().String("app-url", "", "The tinyauth URL.")
|
rootCmd.Flags().String("app-url", "", "The tinyauth URL.")
|
||||||
rootCmd.Flags().String("users", "", "Comma separated list of users in the format email:hash.")
|
rootCmd.Flags().String("users", "", "Comma separated list of users in the format username:hash.")
|
||||||
rootCmd.Flags().String("users-file", "", "Path to a file containing users in the format email:hash.")
|
rootCmd.Flags().String("users-file", "", "Path to a file containing users in the format username:hash.")
|
||||||
rootCmd.Flags().Bool("cookie-secure", false, "Send cookie over secure connection only.")
|
rootCmd.Flags().Bool("cookie-secure", false, "Send cookie over secure connection only.")
|
||||||
rootCmd.Flags().String("github-client-id", "", "Github OAuth client ID.")
|
rootCmd.Flags().String("github-client-id", "", "Github OAuth client ID.")
|
||||||
rootCmd.Flags().String("github-client-secret", "", "Github OAuth client secret.")
|
rootCmd.Flags().String("github-client-secret", "", "Github OAuth client secret.")
|
||||||
|
rootCmd.Flags().String("github-client-secret-file", "", "Github OAuth client secret file.")
|
||||||
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
|
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
|
||||||
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
|
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
|
||||||
|
rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
|
||||||
|
rootCmd.Flags().String("tailscale-client-id", "", "Tailscale OAuth client ID.")
|
||||||
|
rootCmd.Flags().String("tailscale-client-secret", "", "Tailscale OAuth client secret.")
|
||||||
|
rootCmd.Flags().String("tailscale-client-secret-file", "", "Tailscale OAuth client secret file.")
|
||||||
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
|
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
|
||||||
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
|
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
|
||||||
|
rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
|
||||||
rootCmd.Flags().String("generic-scopes", "", "Generic OAuth scopes.")
|
rootCmd.Flags().String("generic-scopes", "", "Generic OAuth scopes.")
|
||||||
rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.")
|
rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.")
|
||||||
rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.")
|
rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.")
|
||||||
@@ -142,25 +163,34 @@ func init() {
|
|||||||
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
|
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
|
||||||
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
|
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
|
||||||
rootCmd.Flags().Int("cookie-expiry", 86400, "Cookie expiration time in seconds.")
|
rootCmd.Flags().Int("cookie-expiry", 86400, "Cookie expiration time in seconds.")
|
||||||
|
rootCmd.Flags().Int("log-level", 1, "Log level.")
|
||||||
viper.BindEnv("port", "PORT")
|
viper.BindEnv("port", "PORT")
|
||||||
viper.BindEnv("address", "ADDRESS")
|
viper.BindEnv("address", "ADDRESS")
|
||||||
viper.BindEnv("secret", "SECRET")
|
viper.BindEnv("secret", "SECRET")
|
||||||
|
viper.BindEnv("secret-file", "SECRET_FILE")
|
||||||
viper.BindEnv("app-url", "APP_URL")
|
viper.BindEnv("app-url", "APP_URL")
|
||||||
viper.BindEnv("users", "USERS")
|
viper.BindEnv("users", "USERS")
|
||||||
viper.BindEnv("users-file", "USERS_FILE")
|
viper.BindEnv("users-file", "USERS_FILE")
|
||||||
viper.BindEnv("cookie-secure", "COOKIE_SECURE")
|
viper.BindEnv("cookie-secure", "COOKIE_SECURE")
|
||||||
viper.BindEnv("github-client-id", "GITHUB_CLIENT_ID")
|
viper.BindEnv("github-client-id", "GITHUB_CLIENT_ID")
|
||||||
viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET")
|
viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET")
|
||||||
|
viper.BindEnv("github-client-secret-file", "GITHUB_CLIENT_SECRET_FILE")
|
||||||
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
|
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
|
||||||
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
|
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
|
||||||
|
viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
|
||||||
|
viper.BindEnv("tailscale-client-id", "TAILSCALE_CLIENT_ID")
|
||||||
|
viper.BindEnv("tailscale-client-secret", "TAILSCALE_CLIENT_SECRET")
|
||||||
|
viper.BindEnv("tailscale-client-secret-file", "TAILSCALE_CLIENT_SECRET_FILE")
|
||||||
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
|
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
|
||||||
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
|
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
|
||||||
|
viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
|
||||||
viper.BindEnv("generic-scopes", "GENERIC_SCOPES")
|
viper.BindEnv("generic-scopes", "GENERIC_SCOPES")
|
||||||
viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL")
|
viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL")
|
||||||
viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL")
|
viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL")
|
||||||
viper.BindEnv("generic-user-url", "GENERIC_USER_URL")
|
viper.BindEnv("generic-user-url", "GENERIC_USER_URL")
|
||||||
viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
|
viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
|
||||||
viper.BindEnv("oauth-whitelist", "WHITELIST")
|
viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST")
|
||||||
viper.BindEnv("cookie-expiry", "COOKIE_EXPIRY")
|
viper.BindEnv("cookie-expiry", "COOKIE_EXPIRY")
|
||||||
|
viper.BindEnv("log-level", "LOG_LEVEL")
|
||||||
viper.BindPFlags(rootCmd.Flags())
|
viper.BindPFlags(rootCmd.Flags())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var interactive bool
|
var interactive bool
|
||||||
var email string
|
var username string
|
||||||
var password string
|
var password string
|
||||||
var docker bool
|
var docker bool
|
||||||
|
|
||||||
@@ -21,12 +22,14 @@ var CreateCmd = &cobra.Command{
|
|||||||
Short: "Create a user",
|
Short: "Create a user",
|
||||||
Long: `Create a user either interactively or by passing flags.`,
|
Long: `Create a user either interactively or by passing flags.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
log.Logger = log.Level(zerolog.InfoLevel)
|
||||||
|
|
||||||
if interactive {
|
if interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
huh.NewInput().Title("Email").Value(&email).Validate((func(s string) error {
|
huh.NewInput().Title("Username").Value(&username).Validate((func(s string) error {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return errors.New("email cannot be empty")
|
return errors.New("username cannot be empty")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})),
|
})),
|
||||||
@@ -49,11 +52,11 @@ var CreateCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if email == "" || password == "" {
|
if username == "" || password == "" {
|
||||||
log.Error().Msg("Email and password cannot be empty")
|
log.Error().Msg("Username and password cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("email", email).Str("password", password).Bool("docker", docker).Msg("Creating user")
|
log.Info().Str("username", username).Str("password", password).Bool("docker", docker).Msg("Creating user")
|
||||||
|
|
||||||
passwordByte, passwordErr := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
passwordByte, passwordErr := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
|
||||||
@@ -67,13 +70,13 @@ var CreateCmd = &cobra.Command{
|
|||||||
passwordString = strings.ReplaceAll(passwordString, "$", "$$")
|
passwordString = strings.ReplaceAll(passwordString, "$", "$$")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("user", fmt.Sprintf("%s:%s", email, passwordString)).Msg("User created")
|
log.Info().Str("user", fmt.Sprintf("%s:%s", username, passwordString)).Msg("User created")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
CreateCmd.Flags().BoolVar(&interactive, "interactive", false, "Create a user interactively")
|
CreateCmd.Flags().BoolVar(&interactive, "interactive", false, "Create a user interactively")
|
||||||
CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker")
|
CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker")
|
||||||
CreateCmd.Flags().StringVar(&email, "email", "", "Email")
|
CreateCmd.Flags().StringVar(&username, "username", "", "Username")
|
||||||
CreateCmd.Flags().StringVar(&password, "password", "", "Password")
|
CreateCmd.Flags().StringVar(&password, "password", "", "Password")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func UserCmd() *cobra.Command {
|
func UserCmd() *cobra.Command {
|
||||||
|
// Create the user command
|
||||||
userCmd := &cobra.Command{
|
userCmd := &cobra.Command{
|
||||||
Use: "user",
|
Use: "user",
|
||||||
Short: "User utilities",
|
Short: "User utilities",
|
||||||
Long: `Utilities for creating and verifying tinyauth compatible users.`,
|
Long: `Utilities for creating and verifying tinyauth compatible users.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add subcommands
|
||||||
userCmd.AddCommand(create.CreateCmd)
|
userCmd.AddCommand(create.CreateCmd)
|
||||||
userCmd.AddCommand(verify.VerifyCmd)
|
userCmd.AddCommand(verify.VerifyCmd)
|
||||||
|
|
||||||
|
// Return the user command
|
||||||
return userCmd
|
return userCmd
|
||||||
}
|
}
|
||||||
@@ -5,13 +5,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var interactive bool
|
var interactive bool
|
||||||
var email string
|
var username string
|
||||||
var password string
|
var password string
|
||||||
var docker bool
|
var docker bool
|
||||||
var user string
|
var user string
|
||||||
@@ -19,20 +20,22 @@ var user string
|
|||||||
var VerifyCmd = &cobra.Command{
|
var VerifyCmd = &cobra.Command{
|
||||||
Use: "verify",
|
Use: "verify",
|
||||||
Short: "Verify a user is set up correctly",
|
Short: "Verify a user is set up correctly",
|
||||||
Long: `Verify a user is set up correctly meaning that it has a correct email and password.`,
|
Long: `Verify a user is set up correctly meaning that it has a correct username and password.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
log.Logger = log.Level(zerolog.InfoLevel)
|
||||||
|
|
||||||
if interactive {
|
if interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
huh.NewInput().Title("User (email:hash)").Value(&user).Validate((func(s string) error {
|
huh.NewInput().Title("User (username:hash)").Value(&user).Validate((func(s string) error {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return errors.New("user cannot be empty")
|
return errors.New("user cannot be empty")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})),
|
})),
|
||||||
huh.NewInput().Title("Email").Value(&email).Validate((func(s string) error {
|
huh.NewInput().Title("Username").Value(&username).Validate((func(s string) error {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return errors.New("email cannot be empty")
|
return errors.New("username cannot be empty")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})),
|
})),
|
||||||
@@ -55,11 +58,11 @@ var VerifyCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if email == "" || password == "" || user == "" {
|
if username == "" || password == "" || user == "" {
|
||||||
log.Fatal().Msg("Email, password and user cannot be empty")
|
log.Fatal().Msg("Username, password and user cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("user", user).Str("email", email).Str("password", password).Bool("docker", docker).Msg("Verifying user")
|
log.Info().Str("user", user).Str("username", username).Str("password", password).Bool("docker", docker).Msg("Verifying user")
|
||||||
|
|
||||||
userSplit := strings.Split(user, ":")
|
userSplit := strings.Split(user, ":")
|
||||||
|
|
||||||
@@ -73,8 +76,8 @@ var VerifyCmd = &cobra.Command{
|
|||||||
|
|
||||||
verifyErr := bcrypt.CompareHashAndPassword([]byte(userSplit[1]), []byte(password))
|
verifyErr := bcrypt.CompareHashAndPassword([]byte(userSplit[1]), []byte(password))
|
||||||
|
|
||||||
if verifyErr != nil || email != userSplit[0] {
|
if verifyErr != nil || username != userSplit[0] {
|
||||||
log.Fatal().Msg("Email or password incorrect")
|
log.Fatal().Msg("Username or password incorrect")
|
||||||
} else {
|
} else {
|
||||||
log.Info().Msg("Verification successful")
|
log.Info().Msg("Verification successful")
|
||||||
}
|
}
|
||||||
@@ -84,7 +87,7 @@ var VerifyCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
|
VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
|
||||||
VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
|
VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
|
||||||
VerifyCmd.Flags().StringVar(&email, "email", "", "Email")
|
VerifyCmd.Flags().StringVar(&username, "username", "", "Username")
|
||||||
VerifyCmd.Flags().StringVar(&password, "password", "", "Password")
|
VerifyCmd.Flags().StringVar(&password, "password", "", "Password")
|
||||||
VerifyCmd.Flags().StringVar(&user, "user", "", "Hash (user:hash combination)")
|
VerifyCmd.Flags().StringVar(&user, "user", "", "Hash (username:hash combination)")
|
||||||
}
|
}
|
||||||
|
|||||||
17
go.mod
17
go.mod
@@ -14,6 +14,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.12.7 // indirect
|
github.com/bytedance/sonic v1.12.7 // indirect
|
||||||
@@ -27,14 +28,22 @@ require (
|
|||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/docker v27.5.1+incompatible // indirect
|
||||||
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.1 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.4 // indirect
|
github.com/goccy/go-json v0.10.4 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/gorilla/sessions v1.2.2 // indirect
|
github.com/gorilla/sessions v1.2.2 // indirect
|
||||||
@@ -51,12 +60,16 @@ require (
|
|||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
|
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // 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.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
@@ -67,6 +80,10 @@ require (
|
|||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/arch v0.13.0 // indirect
|
golang.org/x/arch v0.13.0 // indirect
|
||||||
|
|||||||
69
go.sum
69
go.sum
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||||
|
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
@@ -32,10 +34,20 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
|
||||||
|
github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
@@ -48,6 +60,11 @@ github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E
|
|||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||||
|
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -59,6 +76,8 @@ github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1
|
|||||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
@@ -79,10 +98,13 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -108,6 +130,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
|
|||||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -119,8 +143,14 @@ 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/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
|
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
|
||||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
|
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
@@ -138,6 +168,7 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
|
|||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||||
@@ -151,9 +182,11 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
|||||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -168,31 +201,67 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||||
|
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||||
|
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||||
|
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||||
|
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||||
|
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
||||||
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -42,17 +43,22 @@ type API struct {
|
|||||||
func (api *API) Init() {
|
func (api *API) Init() {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
|
||||||
|
log.Debug().Msg("Setting up router")
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(zerolog())
|
router.Use(zerolog())
|
||||||
|
log.Debug().Msg("Setting up assets")
|
||||||
dist, distErr := fs.Sub(assets.Assets, "dist")
|
dist, distErr := fs.Sub(assets.Assets, "dist")
|
||||||
|
|
||||||
if distErr != nil {
|
if distErr != nil {
|
||||||
log.Fatal().Err(distErr).Msg("Failed to get UI assets")
|
log.Fatal().Err(distErr).Msg("Failed to get UI assets")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Setting up file server")
|
||||||
fileServer := http.FileServer(http.FS(dist))
|
fileServer := http.FileServer(http.FS(dist))
|
||||||
|
log.Debug().Msg("Setting up cookie store")
|
||||||
store := cookie.NewStore([]byte(api.Config.Secret))
|
store := cookie.NewStore([]byte(api.Config.Secret))
|
||||||
|
|
||||||
|
log.Debug().Msg("Getting domain")
|
||||||
domain, domainErr := utils.GetRootURL(api.Config.AppURL)
|
domain, domainErr := utils.GetRootURL(api.Config.AppURL)
|
||||||
|
|
||||||
if domainErr != nil {
|
if domainErr != nil {
|
||||||
@@ -90,18 +96,33 @@ func (api *API) Init() {
|
|||||||
|
|
||||||
func (api *API) SetupRoutes() {
|
func (api *API) SetupRoutes() {
|
||||||
api.Router.GET("/api/auth", func(c *gin.Context) {
|
api.Router.GET("/api/auth", func(c *gin.Context) {
|
||||||
userContext, userContextErr := api.Hooks.UseUserContext(c)
|
log.Debug().Msg("Checking auth")
|
||||||
|
userContext := api.Hooks.UseUserContext(c)
|
||||||
|
|
||||||
if userContextErr != nil {
|
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
||||||
log.Error().Err(userContextErr).Msg("Failed to get user context")
|
proto := c.Request.Header.Get("X-Forwarded-Proto")
|
||||||
c.JSON(500, gin.H{
|
host := c.Request.Header.Get("X-Forwarded-Host")
|
||||||
"status": 500,
|
|
||||||
"message": "Internal Server Error",
|
if userContext.IsLoggedIn {
|
||||||
})
|
log.Debug().Msg("Authenticated")
|
||||||
|
|
||||||
|
appAllowed, appAllowedErr := api.Auth.ResourceAllowed(userContext, host)
|
||||||
|
if handleApiError(c, "Failed to check if resource is allowed", appAllowedErr) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.IsLoggedIn {
|
if !appAllowed {
|
||||||
|
log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User not allowed")
|
||||||
|
queries, queryErr := query.Values(types.UnauthorizedQuery{
|
||||||
|
Username: userContext.Username,
|
||||||
|
Resource: strings.Split(host, ".")[0],
|
||||||
|
})
|
||||||
|
if handleApiError(c, "Failed to build query", queryErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", api.Config.AppURL, queries.Encode()))
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Authenticated",
|
"message": "Authenticated",
|
||||||
@@ -109,13 +130,12 @@ func (api *API) SetupRoutes() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
|
||||||
proto := c.Request.Header.Get("X-Forwarded-Proto")
|
|
||||||
host := c.Request.Header.Get("X-Forwarded-Host")
|
|
||||||
queries, queryErr := query.Values(types.LoginQuery{
|
queries, queryErr := query.Values(types.LoginQuery{
|
||||||
RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri),
|
RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
log.Debug().Interface("redirect_uri", fmt.Sprintf("%s://%s%s", proto, host, uri)).Msg("Redirecting to login")
|
||||||
|
|
||||||
if queryErr != nil {
|
if queryErr != nil {
|
||||||
log.Error().Err(queryErr).Msg("Failed to build query")
|
log.Error().Err(queryErr).Msg("Failed to build query")
|
||||||
c.JSON(501, gin.H{
|
c.JSON(501, gin.H{
|
||||||
@@ -142,9 +162,12 @@ func (api *API) SetupRoutes() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := api.Auth.GetUser(login.Email)
|
log.Debug().Msg("Got login request")
|
||||||
|
|
||||||
|
user := api.Auth.GetUser(login.Username)
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
|
log.Debug().Str("username", login.Username).Msg("User not found")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -153,6 +176,7 @@ func (api *API) SetupRoutes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !api.Auth.CheckPassword(*user, login.Password) {
|
if !api.Auth.CheckPassword(*user, login.Password) {
|
||||||
|
log.Debug().Str("username", login.Username).Msg("Password incorrect")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -160,9 +184,12 @@ func (api *API) SetupRoutes() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session := sessions.Default(c)
|
log.Debug().Msg("Password correct, logging in")
|
||||||
session.Set("tinyauth_sid", fmt.Sprintf("email:%s", login.Email))
|
|
||||||
session.Save()
|
api.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
|
Username: login.Username,
|
||||||
|
Provider: "username",
|
||||||
|
})
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -171,9 +198,9 @@ func (api *API) SetupRoutes() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
api.Router.POST("/api/logout", func(c *gin.Context) {
|
api.Router.POST("/api/logout", func(c *gin.Context) {
|
||||||
session := sessions.Default(c)
|
api.Auth.DeleteSessionCookie(c)
|
||||||
session.Delete("tinyauth_sid")
|
|
||||||
session.Save()
|
log.Debug().Msg("Cleaning up redirect cookie")
|
||||||
|
|
||||||
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true)
|
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true)
|
||||||
|
|
||||||
@@ -184,39 +211,40 @@ func (api *API) SetupRoutes() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
api.Router.GET("/api/status", func(c *gin.Context) {
|
api.Router.GET("/api/status", func(c *gin.Context) {
|
||||||
userContext, userContextErr := api.Hooks.UseUserContext(c)
|
log.Debug().Msg("Checking status")
|
||||||
|
userContext := api.Hooks.UseUserContext(c)
|
||||||
|
|
||||||
if userContextErr != nil {
|
configuredProviders := api.Providers.GetConfiguredProviders()
|
||||||
log.Error().Err(userContextErr).Msg("Failed to get user context")
|
|
||||||
c.JSON(500, gin.H{
|
if api.Auth.UserAuthConfigured() {
|
||||||
"status": 500,
|
configuredProviders = append(configuredProviders, "username")
|
||||||
"message": "Internal Server Error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !userContext.IsLoggedIn {
|
if !userContext.IsLoggedIn {
|
||||||
|
log.Debug().Msg("Unauthenticated")
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Unauthenticated",
|
"message": "Unauthenticated",
|
||||||
"email": "",
|
"username": "",
|
||||||
"isLoggedIn": false,
|
"isLoggedIn": false,
|
||||||
"oauth": false,
|
"oauth": false,
|
||||||
"provider": "",
|
"provider": "",
|
||||||
"configuredProviders": api.Providers.GetConfiguredProviders(),
|
"configuredProviders": configuredProviders,
|
||||||
"disableContinue": api.Config.DisableContinue,
|
"disableContinue": api.Config.DisableContinue,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Interface("userContext", userContext).Strs("configuredProviders", configuredProviders).Bool("disableContinue", api.Config.DisableContinue).Msg("Authenticated")
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Authenticated",
|
"message": "Authenticated",
|
||||||
"email": userContext.Email,
|
"username": userContext.Username,
|
||||||
"isLoggedIn": userContext.IsLoggedIn,
|
"isLoggedIn": userContext.IsLoggedIn,
|
||||||
"oauth": userContext.OAuth,
|
"oauth": userContext.OAuth,
|
||||||
"provider": userContext.Provider,
|
"provider": userContext.Provider,
|
||||||
"configuredProviders": api.Providers.GetConfiguredProviders(),
|
"configuredProviders": configuredProviders,
|
||||||
"disableContinue": api.Config.DisableContinue,
|
"disableContinue": api.Config.DisableContinue,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -242,6 +270,8 @@ func (api *API) SetupRoutes() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got OAuth request")
|
||||||
|
|
||||||
provider := api.Providers.GetProvider(request.Provider)
|
provider := api.Providers.GetProvider(request.Provider)
|
||||||
|
|
||||||
if provider == nil {
|
if provider == nil {
|
||||||
@@ -252,14 +282,34 @@ func (api *API) SetupRoutes() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("provider", request.Provider).Msg("Got provider")
|
||||||
|
|
||||||
authURL := provider.GetAuthURL()
|
authURL := provider.GetAuthURL()
|
||||||
|
|
||||||
|
log.Debug().Msg("Got auth URL")
|
||||||
|
|
||||||
redirectURI := c.Query("redirect_uri")
|
redirectURI := c.Query("redirect_uri")
|
||||||
|
|
||||||
if redirectURI != "" {
|
if redirectURI != "" {
|
||||||
|
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
|
||||||
c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", api.Domain, api.Config.CookieSecure, true)
|
c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", api.Domain, api.Config.CookieSecure, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if request.Provider == "tailscale" {
|
||||||
|
tailscaleQuery, tailscaleQueryErr := query.Values(types.TailscaleQuery{
|
||||||
|
Code: (1000 + rand.IntN(9000)), // doesn't need to be secure, just there to avoid caching
|
||||||
|
})
|
||||||
|
if handleApiError(c, "Failed to build query", tailscaleQueryErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"message": "Ok",
|
||||||
|
"url": fmt.Sprintf("%s/api/oauth/callback/tailscale?%s", api.Config.AppURL, tailscaleQuery.Encode()),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Ok",
|
"message": "Ok",
|
||||||
@@ -276,6 +326,8 @@ func (api *API) SetupRoutes() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name")
|
||||||
|
|
||||||
code := c.Query("code")
|
code := c.Query("code")
|
||||||
|
|
||||||
if code == "" {
|
if code == "" {
|
||||||
@@ -284,14 +336,20 @@ func (api *API) SetupRoutes() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got code")
|
||||||
|
|
||||||
provider := api.Providers.GetProvider(providerName.Provider)
|
provider := api.Providers.GetProvider(providerName.Provider)
|
||||||
|
|
||||||
|
log.Debug().Str("provider", providerName.Provider).Msg("Got provider")
|
||||||
|
|
||||||
if provider == nil {
|
if provider == nil {
|
||||||
c.Redirect(http.StatusPermanentRedirect, "/not-found")
|
c.Redirect(http.StatusPermanentRedirect, "/not-found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, tokenErr := provider.ExchangeToken(code)
|
_, tokenErr := provider.ExchangeToken(code)
|
||||||
|
|
||||||
|
log.Debug().Msg("Got token")
|
||||||
|
|
||||||
if handleApiError(c, "Failed to exchange token", tokenErr) {
|
if handleApiError(c, "Failed to exchange token", tokenErr) {
|
||||||
return
|
return
|
||||||
@@ -299,6 +357,8 @@ func (api *API) SetupRoutes() {
|
|||||||
|
|
||||||
email, emailErr := api.Providers.GetUser(providerName.Provider)
|
email, emailErr := api.Providers.GetUser(providerName.Provider)
|
||||||
|
|
||||||
|
log.Debug().Str("email", email).Msg("Got email")
|
||||||
|
|
||||||
if handleApiError(c, "Failed to get user", emailErr) {
|
if handleApiError(c, "Failed to get user", emailErr) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -306,7 +366,7 @@ func (api *API) SetupRoutes() {
|
|||||||
if !api.Auth.EmailWhitelisted(email) {
|
if !api.Auth.EmailWhitelisted(email) {
|
||||||
log.Warn().Str("email", email).Msg("Email not whitelisted")
|
log.Warn().Str("email", email).Msg("Email not whitelisted")
|
||||||
unauthorizedQuery, unauthorizedQueryErr := query.Values(types.UnauthorizedQuery{
|
unauthorizedQuery, unauthorizedQueryErr := query.Values(types.UnauthorizedQuery{
|
||||||
Email: email,
|
Username: email,
|
||||||
})
|
})
|
||||||
if handleApiError(c, "Failed to build query", unauthorizedQueryErr) {
|
if handleApiError(c, "Failed to build query", unauthorizedQueryErr) {
|
||||||
return
|
return
|
||||||
@@ -314,9 +374,12 @@ func (api *API) SetupRoutes() {
|
|||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/unauthorized?%s", api.Config.AppURL, unauthorizedQuery.Encode()))
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/unauthorized?%s", api.Config.AppURL, unauthorizedQuery.Encode()))
|
||||||
}
|
}
|
||||||
|
|
||||||
session := sessions.Default(c)
|
log.Debug().Msg("Email whitelisted")
|
||||||
session.Set("tinyauth_sid", fmt.Sprintf("%s:%s", providerName.Provider, token))
|
|
||||||
session.Save()
|
api.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
|
Username: email,
|
||||||
|
Provider: providerName.Provider,
|
||||||
|
})
|
||||||
|
|
||||||
redirectURI, redirectURIErr := c.Cookie("tinyauth_redirect_uri")
|
redirectURI, redirectURIErr := c.Cookie("tinyauth_redirect_uri")
|
||||||
|
|
||||||
@@ -327,12 +390,16 @@ func (api *API) SetupRoutes() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("redirectURI", redirectURI).Msg("Got redirect URI")
|
||||||
|
|
||||||
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true)
|
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true)
|
||||||
|
|
||||||
redirectQuery, redirectQueryErr := query.Values(types.LoginQuery{
|
redirectQuery, redirectQueryErr := query.Values(types.LoginQuery{
|
||||||
RedirectURI: redirectURI,
|
RedirectURI: redirectURI,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
log.Debug().Msg("Got redirect query")
|
||||||
|
|
||||||
if handleApiError(c, "Failed to build query", redirectQueryErr) {
|
if handleApiError(c, "Failed to build query", redirectQueryErr) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v1.0.0
|
v2.1.0
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"tinyauth/internal/docker"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
"tinyauth/internal/utils"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAuth(userList types.Users, oauthWhitelist []string) *Auth {
|
func NewAuth(docker *docker.Docker, userList types.Users, oauthWhitelist []string) *Auth {
|
||||||
return &Auth{
|
return &Auth{
|
||||||
|
Docker: docker,
|
||||||
Users: userList,
|
Users: userList,
|
||||||
OAuthWhitelist: oauthWhitelist,
|
OAuthWhitelist: oauthWhitelist,
|
||||||
}
|
}
|
||||||
@@ -15,12 +23,13 @@ func NewAuth(userList types.Users, oauthWhitelist []string) *Auth {
|
|||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
Users types.Users
|
Users types.Users
|
||||||
|
Docker *docker.Docker
|
||||||
OAuthWhitelist []string
|
OAuthWhitelist []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) GetUser(email string) *types.User {
|
func (auth *Auth) GetUser(username string) *types.User {
|
||||||
for _, user := range auth.Users {
|
for _, user := range auth.Users {
|
||||||
if user.Email == email {
|
if user.Username == username {
|
||||||
return &user
|
return &user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,3 +52,96 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) {
|
||||||
|
log.Debug().Msg("Creating session cookie")
|
||||||
|
sessions := sessions.Default(c)
|
||||||
|
log.Debug().Msg("Setting session cookie")
|
||||||
|
sessions.Set("username", data.Username)
|
||||||
|
sessions.Set("provider", data.Provider)
|
||||||
|
sessions.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) DeleteSessionCookie(c *gin.Context) {
|
||||||
|
log.Debug().Msg("Deleting session cookie")
|
||||||
|
sessions := sessions.Default(c)
|
||||||
|
sessions.Clear()
|
||||||
|
sessions.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
|
||||||
|
log.Debug().Msg("Getting session cookie")
|
||||||
|
sessions := sessions.Default(c)
|
||||||
|
|
||||||
|
cookieUsername := sessions.Get("username")
|
||||||
|
cookieProvider := sessions.Get("provider")
|
||||||
|
|
||||||
|
username, usernameOk := cookieUsername.(string)
|
||||||
|
provider, providerOk := cookieProvider.(string)
|
||||||
|
|
||||||
|
log.Debug().Str("username", username).Str("provider", provider).Msg("Parsed cookie")
|
||||||
|
|
||||||
|
if !usernameOk || !providerOk {
|
||||||
|
log.Warn().Msg("Session cookie invalid")
|
||||||
|
return types.SessionCookie{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.SessionCookie{
|
||||||
|
Username: username,
|
||||||
|
Provider: provider,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) UserAuthConfigured() bool {
|
||||||
|
return len(auth.Users) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) ResourceAllowed(context types.UserContext, host string) (bool, error) {
|
||||||
|
appId := strings.Split(host, ".")[0]
|
||||||
|
containers, containersErr := auth.Docker.GetContainers()
|
||||||
|
|
||||||
|
if containersErr != nil {
|
||||||
|
return false, containersErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got containers")
|
||||||
|
|
||||||
|
for _, container := range containers {
|
||||||
|
inspect, inspectErr := auth.Docker.InspectContainer(container.ID)
|
||||||
|
|
||||||
|
if inspectErr != nil {
|
||||||
|
return false, inspectErr
|
||||||
|
}
|
||||||
|
|
||||||
|
containerName := strings.Split(inspect.Name, "/")[1]
|
||||||
|
|
||||||
|
if containerName == appId {
|
||||||
|
log.Debug().Str("container", containerName).Msg("Found container")
|
||||||
|
|
||||||
|
labels := utils.GetTinyauthLabels(inspect.Config.Labels)
|
||||||
|
|
||||||
|
log.Debug().Msg("Got labels")
|
||||||
|
|
||||||
|
if context.OAuth && len(labels.OAuthWhitelist) != 0 {
|
||||||
|
log.Debug().Msg("Checking OAuth whitelist")
|
||||||
|
if slices.Contains(labels.OAuthWhitelist, context.Username) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(labels.Users) != 0 {
|
||||||
|
log.Debug().Msg("Checking users")
|
||||||
|
if slices.Contains(labels.Users, context.Username) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("No matching container found, allowing access")
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|||||||
6
internal/constants/constants.go
Normal file
6
internal/constants/constants.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
var TinyauthLabels = []string{
|
||||||
|
"tinyauth.oauth.whitelist",
|
||||||
|
"tinyauth.users",
|
||||||
|
}
|
||||||
51
internal/docker/docker.go
Normal file
51
internal/docker/docker.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewDocker() *Docker {
|
||||||
|
return &Docker{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Docker struct {
|
||||||
|
Client *client.Client
|
||||||
|
Context context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (docker *Docker) Init() error {
|
||||||
|
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
docker.Context = context.Background()
|
||||||
|
docker.Client = apiClient
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (docker *Docker) GetContainers() ([]types.Container, error) {
|
||||||
|
containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return containers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (docker *Docker) InspectContainer(containerId string) (types.ContainerJSON, error) {
|
||||||
|
inspect, err := docker.Client.ContainerInspect(docker.Context, containerId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return types.ContainerJSON{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return inspect, nil
|
||||||
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
package hooks
|
package hooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"tinyauth/internal/auth"
|
"tinyauth/internal/auth"
|
||||||
"tinyauth/internal/providers"
|
"tinyauth/internal/providers"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"golang.org/x/oauth2"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks {
|
func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks {
|
||||||
@@ -23,103 +21,60 @@ type Hooks struct {
|
|||||||
Providers *providers.Providers
|
Providers *providers.Providers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hooks *Hooks) UseUserContext(c *gin.Context) (types.UserContext, error) {
|
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
|
||||||
session := sessions.Default(c)
|
cookie, cookiErr := hooks.Auth.GetSessionCookie(c)
|
||||||
sessionCookie := session.Get("tinyauth_sid")
|
|
||||||
|
|
||||||
if sessionCookie == nil {
|
if cookiErr != nil {
|
||||||
|
log.Error().Err(cookiErr).Msg("Failed to get session cookie")
|
||||||
return types.UserContext{
|
return types.UserContext{
|
||||||
Email: "",
|
Username: "",
|
||||||
IsLoggedIn: false,
|
IsLoggedIn: false,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "",
|
Provider: "",
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data, dataOk := sessionCookie.(string)
|
if cookie.Provider == "username" {
|
||||||
|
log.Debug().Msg("Provider is username")
|
||||||
if !dataOk {
|
if hooks.Auth.GetUser(cookie.Username) != nil {
|
||||||
|
log.Debug().Msg("User exists")
|
||||||
return types.UserContext{
|
return types.UserContext{
|
||||||
Email: "",
|
Username: cookie.Username,
|
||||||
IsLoggedIn: false,
|
|
||||||
OAuth: false,
|
|
||||||
Provider: "",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
split := strings.Split(data, ":")
|
|
||||||
|
|
||||||
if len(split) != 2 {
|
|
||||||
return types.UserContext{
|
|
||||||
Email: "",
|
|
||||||
IsLoggedIn: false,
|
|
||||||
OAuth: false,
|
|
||||||
Provider: "",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionType := split[0]
|
|
||||||
sessionValue := split[1]
|
|
||||||
|
|
||||||
if sessionType == "email" {
|
|
||||||
user := hooks.Auth.GetUser(sessionValue)
|
|
||||||
if user == nil {
|
|
||||||
return types.UserContext{
|
|
||||||
Email: "",
|
|
||||||
IsLoggedIn: false,
|
|
||||||
OAuth: false,
|
|
||||||
Provider: "",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return types.UserContext{
|
|
||||||
Email: sessionValue,
|
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "",
|
Provider: "",
|
||||||
}, nil
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
provider := hooks.Providers.GetProvider(sessionType)
|
log.Debug().Msg("Provider is not username")
|
||||||
|
provider := hooks.Providers.GetProvider(cookie.Provider)
|
||||||
|
|
||||||
if provider == nil {
|
if provider != nil {
|
||||||
|
log.Debug().Msg("Provider exists")
|
||||||
|
if !hooks.Auth.EmailWhitelisted(cookie.Username) {
|
||||||
|
log.Error().Str("email", cookie.Username).Msg("Email is not whitelisted")
|
||||||
|
hooks.Auth.DeleteSessionCookie(c)
|
||||||
return types.UserContext{
|
return types.UserContext{
|
||||||
Email: "",
|
Username: "",
|
||||||
IsLoggedIn: false,
|
IsLoggedIn: false,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "",
|
Provider: "",
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.Token = &oauth2.Token{
|
|
||||||
AccessToken: sessionValue,
|
|
||||||
}
|
}
|
||||||
|
log.Debug().Msg("Email is whitelisted")
|
||||||
email, emailErr := hooks.Providers.GetUser(sessionType)
|
|
||||||
|
|
||||||
if emailErr != nil {
|
|
||||||
return types.UserContext{
|
return types.UserContext{
|
||||||
Email: "",
|
Username: cookie.Username,
|
||||||
IsLoggedIn: false,
|
|
||||||
OAuth: false,
|
|
||||||
Provider: "",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hooks.Auth.EmailWhitelisted(email) {
|
|
||||||
session.Delete("tinyauth_sid")
|
|
||||||
session.Save()
|
|
||||||
return types.UserContext{
|
|
||||||
Email: "",
|
|
||||||
IsLoggedIn: false,
|
|
||||||
OAuth: false,
|
|
||||||
Provider: "",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return types.UserContext{
|
|
||||||
Email: email,
|
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
OAuth: true,
|
OAuth: true,
|
||||||
Provider: sessionType,
|
Provider: cookie.Provider,
|
||||||
}, nil
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.UserContext{
|
||||||
|
Username: "",
|
||||||
|
IsLoggedIn: false,
|
||||||
|
OAuth: false,
|
||||||
|
Provider: "",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GenericUserInfoResponse struct {
|
type GenericUserInfoResponse struct {
|
||||||
@@ -17,12 +19,16 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
|
|||||||
return "", resErr
|
return "", resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got response from generic provider")
|
||||||
|
|
||||||
body, bodyErr := io.ReadAll(res.Body)
|
body, bodyErr := io.ReadAll(res.Body)
|
||||||
|
|
||||||
if bodyErr != nil {
|
if bodyErr != nil {
|
||||||
return "", bodyErr
|
return "", bodyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Read body from generic provider")
|
||||||
|
|
||||||
var user GenericUserInfoResponse
|
var user GenericUserInfoResponse
|
||||||
|
|
||||||
jsonErr := json.Unmarshal(body, &user)
|
jsonErr := json.Unmarshal(body, &user)
|
||||||
@@ -31,5 +37,7 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
|
|||||||
return "", jsonErr
|
return "", jsonErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Parsed user from generic provider")
|
||||||
|
|
||||||
return user.Email, nil
|
return user.Email, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GithubUserInfoResponse []struct {
|
type GithubUserInfoResponse []struct {
|
||||||
@@ -23,12 +25,16 @@ func GetGithubEmail(client *http.Client) (string, error) {
|
|||||||
return "", resErr
|
return "", resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got response from github")
|
||||||
|
|
||||||
body, bodyErr := io.ReadAll(res.Body)
|
body, bodyErr := io.ReadAll(res.Body)
|
||||||
|
|
||||||
if bodyErr != nil {
|
if bodyErr != nil {
|
||||||
return "", bodyErr
|
return "", bodyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Read body from github")
|
||||||
|
|
||||||
var emails GithubUserInfoResponse
|
var emails GithubUserInfoResponse
|
||||||
|
|
||||||
jsonErr := json.Unmarshal(body, &emails)
|
jsonErr := json.Unmarshal(body, &emails)
|
||||||
@@ -37,6 +43,8 @@ func GetGithubEmail(client *http.Client) (string, error) {
|
|||||||
return "", jsonErr
|
return "", jsonErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Parsed emails from github")
|
||||||
|
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
if email.Primary {
|
if email.Primary {
|
||||||
return email.Email, nil
|
return email.Email, nil
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GoogleUserInfoResponse struct {
|
type GoogleUserInfoResponse struct {
|
||||||
@@ -21,12 +23,16 @@ func GetGoogleEmail(client *http.Client) (string, error) {
|
|||||||
return "", resErr
|
return "", resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got response from google")
|
||||||
|
|
||||||
body, bodyErr := io.ReadAll(res.Body)
|
body, bodyErr := io.ReadAll(res.Body)
|
||||||
|
|
||||||
if bodyErr != nil {
|
if bodyErr != nil {
|
||||||
return "", bodyErr
|
return "", bodyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Read body from google")
|
||||||
|
|
||||||
var user GoogleUserInfoResponse
|
var user GoogleUserInfoResponse
|
||||||
|
|
||||||
jsonErr := json.Unmarshal(body, &user)
|
jsonErr := json.Unmarshal(body, &user)
|
||||||
@@ -35,5 +41,7 @@ func GetGoogleEmail(client *http.Client) (string, error) {
|
|||||||
return "", jsonErr
|
return "", jsonErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Parsed user from google")
|
||||||
|
|
||||||
return user.Email, nil
|
return user.Email, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type Providers struct {
|
|||||||
Config types.OAuthConfig
|
Config types.OAuthConfig
|
||||||
Github *oauth.OAuth
|
Github *oauth.OAuth
|
||||||
Google *oauth.OAuth
|
Google *oauth.OAuth
|
||||||
|
Tailscale *oauth.OAuth
|
||||||
Generic *oauth.OAuth
|
Generic *oauth.OAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +47,17 @@ func (providers *Providers) Init() {
|
|||||||
})
|
})
|
||||||
providers.Google.Init()
|
providers.Google.Init()
|
||||||
}
|
}
|
||||||
|
if providers.Config.TailscaleClientId != "" && providers.Config.TailscaleClientSecret != "" {
|
||||||
|
log.Info().Msg("Initializing Tailscale OAuth")
|
||||||
|
providers.Tailscale = oauth.NewOAuth(oauth2.Config{
|
||||||
|
ClientID: providers.Config.TailscaleClientId,
|
||||||
|
ClientSecret: providers.Config.TailscaleClientSecret,
|
||||||
|
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/tailscale", providers.Config.AppURL),
|
||||||
|
Scopes: TailscaleScopes(),
|
||||||
|
Endpoint: TailscaleEndpoint,
|
||||||
|
})
|
||||||
|
providers.Tailscale.Init()
|
||||||
|
}
|
||||||
if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" {
|
if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" {
|
||||||
log.Info().Msg("Initializing Generic OAuth")
|
log.Info().Msg("Initializing Generic OAuth")
|
||||||
providers.Generic = oauth.NewOAuth(oauth2.Config{
|
providers.Generic = oauth.NewOAuth(oauth2.Config{
|
||||||
@@ -68,6 +80,8 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
|
|||||||
return providers.Github
|
return providers.Github
|
||||||
case "google":
|
case "google":
|
||||||
return providers.Google
|
return providers.Google
|
||||||
|
case "tailscale":
|
||||||
|
return providers.Tailscale
|
||||||
case "generic":
|
case "generic":
|
||||||
return providers.Generic
|
return providers.Generic
|
||||||
default:
|
default:
|
||||||
@@ -79,33 +93,55 @@ func (providers *Providers) GetUser(provider string) (string, error) {
|
|||||||
switch provider {
|
switch provider {
|
||||||
case "github":
|
case "github":
|
||||||
if providers.Github == nil {
|
if providers.Github == nil {
|
||||||
|
log.Debug().Msg("Github provider not configured")
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
client := providers.Github.GetClient()
|
client := providers.Github.GetClient()
|
||||||
|
log.Debug().Msg("Got client from github")
|
||||||
email, emailErr := GetGithubEmail(client)
|
email, emailErr := GetGithubEmail(client)
|
||||||
if emailErr != nil {
|
if emailErr != nil {
|
||||||
return "", emailErr
|
return "", emailErr
|
||||||
}
|
}
|
||||||
|
log.Debug().Msg("Got email from github")
|
||||||
return email, nil
|
return email, nil
|
||||||
case "google":
|
case "google":
|
||||||
if providers.Google == nil {
|
if providers.Google == nil {
|
||||||
|
log.Debug().Msg("Google provider not configured")
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
client := providers.Google.GetClient()
|
client := providers.Google.GetClient()
|
||||||
|
log.Debug().Msg("Got client from google")
|
||||||
email, emailErr := GetGoogleEmail(client)
|
email, emailErr := GetGoogleEmail(client)
|
||||||
if emailErr != nil {
|
if emailErr != nil {
|
||||||
return "", emailErr
|
return "", emailErr
|
||||||
}
|
}
|
||||||
|
log.Debug().Msg("Got email from google")
|
||||||
|
return email, nil
|
||||||
|
case "tailscale":
|
||||||
|
if providers.Tailscale == nil {
|
||||||
|
log.Debug().Msg("Tailscale provider not configured")
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
client := providers.Tailscale.GetClient()
|
||||||
|
log.Debug().Msg("Got client from tailscale")
|
||||||
|
email, emailErr := GetTailscaleEmail(client)
|
||||||
|
if emailErr != nil {
|
||||||
|
return "", emailErr
|
||||||
|
}
|
||||||
|
log.Debug().Msg("Got email from tailscale")
|
||||||
return email, nil
|
return email, nil
|
||||||
case "generic":
|
case "generic":
|
||||||
if providers.Generic == nil {
|
if providers.Generic == nil {
|
||||||
|
log.Debug().Msg("Generic provider not configured")
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
client := providers.Generic.GetClient()
|
client := providers.Generic.GetClient()
|
||||||
|
log.Debug().Msg("Got client from generic")
|
||||||
email, emailErr := GetGenericEmail(client, providers.Config.GenericUserURL)
|
email, emailErr := GetGenericEmail(client, providers.Config.GenericUserURL)
|
||||||
if emailErr != nil {
|
if emailErr != nil {
|
||||||
return "", emailErr
|
return "", emailErr
|
||||||
}
|
}
|
||||||
|
log.Debug().Msg("Got email from generic")
|
||||||
return email, nil
|
return email, nil
|
||||||
default:
|
default:
|
||||||
return "", nil
|
return "", nil
|
||||||
@@ -120,6 +156,9 @@ func (provider *Providers) GetConfiguredProviders() []string {
|
|||||||
if provider.Google != nil {
|
if provider.Google != nil {
|
||||||
providers = append(providers, "google")
|
providers = append(providers, "google")
|
||||||
}
|
}
|
||||||
|
if provider.Tailscale != nil {
|
||||||
|
providers = append(providers, "tailscale")
|
||||||
|
}
|
||||||
if provider.Generic != nil {
|
if provider.Generic != nil {
|
||||||
providers = append(providers, "generic")
|
providers = append(providers, "generic")
|
||||||
}
|
}
|
||||||
|
|||||||
56
internal/providers/tailscale.go
Normal file
56
internal/providers/tailscale.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TailscaleUser struct {
|
||||||
|
LoginName string `json:"loginName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TailscaleUserInfoResponse struct {
|
||||||
|
Users []TailscaleUser `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TailscaleScopes() []string {
|
||||||
|
return []string{"users:read"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var TailscaleEndpoint = oauth2.Endpoint{
|
||||||
|
TokenURL: "https://api.tailscale.com/api/v2/oauth/token",
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTailscaleEmail(client *http.Client) (string, error) {
|
||||||
|
res, resErr := client.Get("https://api.tailscale.com/api/v2/tailnet/-/users")
|
||||||
|
|
||||||
|
if resErr != nil {
|
||||||
|
return "", resErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got response from tailscale")
|
||||||
|
|
||||||
|
body, bodyErr := io.ReadAll(res.Body)
|
||||||
|
|
||||||
|
if bodyErr != nil {
|
||||||
|
return "", bodyErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Read body from tailscale")
|
||||||
|
|
||||||
|
var users TailscaleUserInfoResponse
|
||||||
|
|
||||||
|
jsonErr := json.Unmarshal(body, &users)
|
||||||
|
|
||||||
|
if jsonErr != nil {
|
||||||
|
return "", jsonErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Parsed users from tailscale")
|
||||||
|
|
||||||
|
return users.Users[0].LoginName, nil
|
||||||
|
}
|
||||||
@@ -7,42 +7,50 @@ type LoginQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
Email string `json:"email"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Email string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Users []User
|
type Users []User
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port int `validate:"number" mapstructure:"port"`
|
Port int `mapstructure:"port" validate:"required"`
|
||||||
Address string `mapstructure:"address, ip4_addr"`
|
Address string `validate:"required,ip4_addr" mapstructure:"address"`
|
||||||
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
||||||
|
SecretFile string `mapstructure:"secret-file"`
|
||||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||||
Users string `mapstructure:"users"`
|
Users string `mapstructure:"users"`
|
||||||
UsersFile string `mapstructure:"users-file"`
|
UsersFile string `mapstructure:"users-file"`
|
||||||
CookieSecure bool `mapstructure:"cookie-secure"`
|
CookieSecure bool `mapstructure:"cookie-secure"`
|
||||||
GithubClientId string `mapstructure:"github-client-id"`
|
GithubClientId string `mapstructure:"github-client-id"`
|
||||||
GithubClientSecret string `mapstructure:"github-client-secret"`
|
GithubClientSecret string `mapstructure:"github-client-secret"`
|
||||||
|
GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
|
||||||
GoogleClientId string `mapstructure:"google-client-id"`
|
GoogleClientId string `mapstructure:"google-client-id"`
|
||||||
GoogleClientSecret string `mapstructure:"google-client-secret"`
|
GoogleClientSecret string `mapstructure:"google-client-secret"`
|
||||||
|
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
|
||||||
|
TailscaleClientId string `mapstructure:"tailscale-client-id"`
|
||||||
|
TailscaleClientSecret string `mapstructure:"tailscale-client-secret"`
|
||||||
|
TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"`
|
||||||
GenericClientId string `mapstructure:"generic-client-id"`
|
GenericClientId string `mapstructure:"generic-client-id"`
|
||||||
GenericClientSecret string `mapstructure:"generic-client-secret"`
|
GenericClientSecret string `mapstructure:"generic-client-secret"`
|
||||||
|
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
|
||||||
GenericScopes string `mapstructure:"generic-scopes"`
|
GenericScopes string `mapstructure:"generic-scopes"`
|
||||||
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
||||||
GenericTokenURL string `mapstructure:"generic-token-url"`
|
GenericTokenURL string `mapstructure:"generic-token-url"`
|
||||||
GenericUserURL string `mapstructure:"generic-user-info-url"`
|
GenericUserURL string `mapstructure:"generic-user-url"`
|
||||||
DisableContinue bool `mapstructure:"disable-continue"`
|
DisableContinue bool `mapstructure:"disable-continue"`
|
||||||
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
||||||
CookieExpiry int `mapstructure:"cookie-expiry"`
|
CookieExpiry int `mapstructure:"cookie-expiry"`
|
||||||
|
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserContext struct {
|
type UserContext struct {
|
||||||
Email string
|
Username string
|
||||||
IsLoggedIn bool
|
IsLoggedIn bool
|
||||||
OAuth bool
|
OAuth bool
|
||||||
Provider string
|
Provider string
|
||||||
@@ -63,6 +71,8 @@ type OAuthConfig struct {
|
|||||||
GithubClientSecret string
|
GithubClientSecret string
|
||||||
GoogleClientId string
|
GoogleClientId string
|
||||||
GoogleClientSecret string
|
GoogleClientSecret string
|
||||||
|
TailscaleClientId string
|
||||||
|
TailscaleClientSecret string
|
||||||
GenericClientId string
|
GenericClientId string
|
||||||
GenericClientSecret string
|
GenericClientSecret string
|
||||||
GenericScopes []string
|
GenericScopes []string
|
||||||
@@ -83,5 +93,20 @@ type OAuthProviders struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UnauthorizedQuery struct {
|
type UnauthorizedQuery struct {
|
||||||
Email string `url:"email"`
|
Username string `url:"username"`
|
||||||
|
Resource string `url:"resource"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionCookie struct {
|
||||||
|
Username string
|
||||||
|
Provider string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TinyauthLabels struct {
|
||||||
|
OAuthWhitelist []string
|
||||||
|
Users []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TailscaleQuery struct {
|
||||||
|
Code int `url:"code"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"tinyauth/internal/constants"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseUsers(users string) (types.Users, error) {
|
func ParseUsers(users string) (types.Users, error) {
|
||||||
|
log.Debug().Msg("Parsing users")
|
||||||
var usersParsed types.Users
|
var usersParsed types.Users
|
||||||
userList := strings.Split(users, ",")
|
userList := strings.Split(users, ",")
|
||||||
|
|
||||||
@@ -22,11 +27,13 @@ func ParseUsers(users string) (types.Users, error) {
|
|||||||
return types.Users{}, errors.New("invalid user format")
|
return types.Users{}, errors.New("invalid user format")
|
||||||
}
|
}
|
||||||
usersParsed = append(usersParsed, types.User{
|
usersParsed = append(usersParsed, types.User{
|
||||||
Email: userSplit[0],
|
Username: userSplit[0],
|
||||||
Password: userSplit[1],
|
Password: userSplit[1],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Parsed users")
|
||||||
|
|
||||||
return usersParsed, nil
|
return usersParsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,21 +44,21 @@ func GetRootURL(urlSrc string) (string, error) {
|
|||||||
return "", parseErr
|
return "", parseErr
|
||||||
}
|
}
|
||||||
|
|
||||||
urlSplitted := strings.Split(urlParsed.Host, ".")
|
urlSplitted := strings.Split(urlParsed.Hostname(), ".")
|
||||||
|
|
||||||
urlFinal := strings.Join(urlSplitted[1:], ".")
|
urlFinal := strings.Join(urlSplitted[1:], ".")
|
||||||
|
|
||||||
return urlFinal, nil
|
return urlFinal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUsersFromFile(usersFile string) (string, error) {
|
func ReadFile(file string) (string, error) {
|
||||||
_, statErr := os.Stat(usersFile)
|
_, statErr := os.Stat(file)
|
||||||
|
|
||||||
if statErr != nil {
|
if statErr != nil {
|
||||||
return "", statErr
|
return "", statErr
|
||||||
}
|
}
|
||||||
|
|
||||||
data, readErr := os.ReadFile(usersFile)
|
data, readErr := os.ReadFile(file)
|
||||||
|
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
return "", readErr
|
return "", readErr
|
||||||
@@ -69,15 +76,73 @@ func ParseFileToLine(content string) string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
users = append(users, line)
|
users = append(users, strings.TrimSpace(line))
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(users, ",")
|
return strings.Join(users, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseCommaString(str string) []string {
|
func GetSecret(conf string, file string) string {
|
||||||
if str == "" {
|
if conf == "" && file == "" {
|
||||||
return []string{}
|
return ""
|
||||||
}
|
}
|
||||||
return strings.Split(str, ",")
|
|
||||||
|
if conf != "" {
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
|
contents, err := ReadFile(file)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUsers(conf string, file string) (types.Users, error) {
|
||||||
|
var users string
|
||||||
|
|
||||||
|
if conf == "" && file == "" {
|
||||||
|
return types.Users{}, errors.New("no users provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf != "" {
|
||||||
|
log.Debug().Msg("Using users from config")
|
||||||
|
users += conf
|
||||||
|
}
|
||||||
|
|
||||||
|
if file != "" {
|
||||||
|
fileContents, fileErr := ReadFile(file)
|
||||||
|
|
||||||
|
if fileErr == nil {
|
||||||
|
log.Debug().Msg("Using users from file")
|
||||||
|
if users != "" {
|
||||||
|
users += ","
|
||||||
|
}
|
||||||
|
users += ParseFileToLine(fileContents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseUsers(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OAuthConfigured(config types.Config) bool {
|
||||||
|
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
|
||||||
|
var tinyauthLabels types.TinyauthLabels
|
||||||
|
for label, value := range labels {
|
||||||
|
if slices.Contains(constants.TinyauthLabels, label) {
|
||||||
|
log.Debug().Str("label", label).Msg("Found label")
|
||||||
|
switch label {
|
||||||
|
case "tinyauth.oauth.whitelist":
|
||||||
|
tinyauthLabels.OAuthWhitelist = strings.Split(value, ",")
|
||||||
|
case "tinyauth.users":
|
||||||
|
tinyauthLabels.Users = strings.Split(value, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tinyauthLabels
|
||||||
}
|
}
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -4,7 +4,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"tinyauth/cmd"
|
"tinyauth/cmd"
|
||||||
"tinyauth/internal/assets"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -12,8 +11,7 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Logger
|
// Logger
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger()
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.FatalLevel)
|
||||||
log.Info().Str("version", assets.Version).Msg("Starting tinyauth")
|
|
||||||
|
|
||||||
// Run cmd
|
// Run cmd
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
|
|||||||
BIN
site/bun.lockb
BIN
site/bun.lockb
Binary file not shown.
55
site/src/icons/tailscale.tsx
Normal file
55
site/src/icons/tailscale.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
|
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<style>{".st0{opacity:0.2;fill:#CCCAC9;}.st1{fill:#FFFFFF;}"}</style>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
className="st0"
|
||||||
|
d="M65.6,127.7c35.3,0,63.9-28.6,63.9-63.9S100.9,0,65.6,0S1.8,28.6,1.8,63.9S30.4,127.7,65.6,127.7z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="st1"
|
||||||
|
d="M65.6,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,219,1.8,254.2S30.4,318.1,65.6,318.1z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="st0"
|
||||||
|
d="M65.6,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,412.9,1.8,448.1S30.4,512,65.6,512z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="st1"
|
||||||
|
d="M257.2,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,318.1,257.2,318.1z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="st1"
|
||||||
|
d="M257.2,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,512,257.2,512z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="st0"
|
||||||
|
d="M257.2,127.7c35.3,0,63.9-28.6,63.9-63.9S292.5,0,257.2,0s-63.9,28.6-63.9,63.9S221.9,127.7,257.2,127.7z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="st0"
|
||||||
|
d="M446.4,127.7c35.3,0,63.9-28.6,63.9-63.9S481.6,0,446.4,0c-35.3,0-63.9,28.6-63.9,63.9S411.1,127.7,446.4,127.7z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="st1"
|
||||||
|
d="M446.4,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,318.1,446.4,318.1z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="st0"
|
||||||
|
d="M446.4,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,512,446.4,512z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Button, Paper, Text } from "@mantine/core";
|
import { Button, Code, Paper, Text } from "@mantine/core";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { Navigate } from "react-router";
|
import { Navigate } from "react-router";
|
||||||
import { useUserContext } from "../context/user-context";
|
import { useUserContext } from "../context/user-context";
|
||||||
import { Layout } from "../components/layouts/layout";
|
import { Layout } from "../components/layouts/layout";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
export const ContinuePage = () => {
|
export const ContinuePage = () => {
|
||||||
const queryString = window.location.search;
|
const queryString = window.location.search;
|
||||||
@@ -12,11 +13,11 @@ export const ContinuePage = () => {
|
|||||||
const { isLoggedIn, disableContinue } = useUserContext();
|
const { isLoggedIn, disableContinue } = useUserContext();
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return <Navigate to="/login" />;
|
return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disableContinue && redirectUri !== "null") {
|
if (redirectUri === "null") {
|
||||||
window.location.replace(redirectUri!);
|
return <Navigate to="/" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirect = () => {
|
const redirect = () => {
|
||||||
@@ -26,15 +27,46 @@ export const ContinuePage = () => {
|
|||||||
color: "blue",
|
color: "blue",
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.replace(redirectUri!);
|
window.location.href = redirectUri!;
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const urlParsed = URL.parse(redirectUri!);
|
||||||
|
|
||||||
|
if (
|
||||||
|
window.location.protocol === "https:" &&
|
||||||
|
urlParsed!.protocol === "http:"
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<ContinuePageLayout>
|
||||||
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
|
<Text size="xl" fw={700}>
|
||||||
{redirectUri !== "null" ? (
|
Insecure Redirect
|
||||||
<>
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Your are logged in but trying to redirect from <Code>https</Code> to{" "}
|
||||||
|
<Code>http</Code>, please click the button to redirect.
|
||||||
|
</Text>
|
||||||
|
<Button fullWidth mt="xl" onClick={redirect}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</ContinuePageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableContinue) {
|
||||||
|
window.location.href = redirectUri!;
|
||||||
|
return (
|
||||||
|
<ContinuePageLayout>
|
||||||
|
<Text size="xl" fw={700}>
|
||||||
|
Redirecting
|
||||||
|
</Text>
|
||||||
|
<Text>You should be redirected to your app soon.</Text>
|
||||||
|
</ContinuePageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContinuePageLayout>
|
||||||
<Text size="xl" fw={700}>
|
<Text size="xl" fw={700}>
|
||||||
Continue
|
Continue
|
||||||
</Text>
|
</Text>
|
||||||
@@ -42,15 +74,15 @@ export const ContinuePage = () => {
|
|||||||
<Button fullWidth mt="xl" onClick={redirect}>
|
<Button fullWidth mt="xl" onClick={redirect}>
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</ContinuePageLayout>
|
||||||
) : (
|
);
|
||||||
<>
|
};
|
||||||
<Text size="xl" fw={700}>
|
|
||||||
Logged in
|
export const ContinuePageLayout = ({ children }: { children: ReactNode }) => {
|
||||||
</Text>
|
return (
|
||||||
<Text>You are now signed in and can use your apps.</Text>
|
<Layout>
|
||||||
</>
|
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
|
||||||
)}
|
{children}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Layout } from "../components/layouts/layout";
|
|||||||
import { GoogleIcon } from "../icons/google";
|
import { GoogleIcon } from "../icons/google";
|
||||||
import { GithubIcon } from "../icons/github";
|
import { GithubIcon } from "../icons/github";
|
||||||
import { OAuthIcon } from "../icons/oauth";
|
import { OAuthIcon } from "../icons/oauth";
|
||||||
|
import { TailscaleIcon } from "../icons/tailscale";
|
||||||
|
|
||||||
export const LoginPage = () => {
|
export const LoginPage = () => {
|
||||||
const queryString = window.location.search;
|
const queryString = window.location.search;
|
||||||
@@ -26,13 +27,16 @@ export const LoginPage = () => {
|
|||||||
const redirectUri = params.get("redirect_uri");
|
const redirectUri = params.get("redirect_uri");
|
||||||
|
|
||||||
const { isLoggedIn, configuredProviders } = useUserContext();
|
const { isLoggedIn, configuredProviders } = useUserContext();
|
||||||
|
const oauthProviders = configuredProviders.filter(
|
||||||
|
(value) => value !== "username",
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
return <Navigate to="/logout" />;
|
return <Navigate to="/logout" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
email: z.string().email(),
|
username: z.string(),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,7 +45,7 @@ export const LoginPage = () => {
|
|||||||
const form = useForm({
|
const form = useForm({
|
||||||
mode: "uncontrolled",
|
mode: "uncontrolled",
|
||||||
initialValues: {
|
initialValues: {
|
||||||
email: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
validate: zodResolver(schema),
|
validate: zodResolver(schema),
|
||||||
@@ -54,7 +58,7 @@ export const LoginPage = () => {
|
|||||||
onError: () => {
|
onError: () => {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: "Failed to login",
|
title: "Failed to login",
|
||||||
message: "Check your email and password",
|
message: "Check your username and password",
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -65,8 +69,12 @@ export const LoginPage = () => {
|
|||||||
color: "green",
|
color: "green",
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (redirectUri === "null") {
|
||||||
|
window.location.replace("/");
|
||||||
|
} else {
|
||||||
window.location.replace(`/continue?redirect_uri=${redirectUri}`);
|
window.location.replace(`/continue?redirect_uri=${redirectUri}`);
|
||||||
});
|
}
|
||||||
|
}, 500);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +92,14 @@ export const LoginPage = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
window.location.replace(data.data.url);
|
notifications.show({
|
||||||
|
title: "Redirecting",
|
||||||
|
message: "Redirecting to your OAuth provider",
|
||||||
|
color: "blue",
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = data.data.url;
|
||||||
|
}, 500);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,18 +111,13 @@ export const LoginPage = () => {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Title ta="center">Tinyauth</Title>
|
<Title ta="center">Tinyauth</Title>
|
||||||
<Paper shadow="md" p="xl" mt={30} radius="md" withBorder>
|
<Paper shadow="md" p="xl" mt={30} radius="md" withBorder>
|
||||||
{configuredProviders.length === 0 && (
|
{oauthProviders.length > 0 && (
|
||||||
<Text size="lg" mb="md" fw={500} ta="center">
|
|
||||||
Welcome back, please login
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{configuredProviders.length > 0 && (
|
|
||||||
<>
|
<>
|
||||||
<Text size="lg" fw={500} ta="center">
|
<Text size="lg" fw={500} ta="center">
|
||||||
Welcome back, login with
|
Welcome back, login with
|
||||||
</Text>
|
</Text>
|
||||||
<Grid mb="md" mt="md" align="center" justify="center">
|
<Grid mb="md" mt="md" align="center" justify="center">
|
||||||
{configuredProviders.includes("google") && (
|
{oauthProviders.includes("google") && (
|
||||||
<Grid.Col span="content">
|
<Grid.Col span="content">
|
||||||
<Button
|
<Button
|
||||||
radius="xl"
|
radius="xl"
|
||||||
@@ -122,7 +132,7 @@ export const LoginPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
)}
|
)}
|
||||||
{configuredProviders.includes("github") && (
|
{oauthProviders.includes("github") && (
|
||||||
<Grid.Col span="content">
|
<Grid.Col span="content">
|
||||||
<Button
|
<Button
|
||||||
radius="xl"
|
radius="xl"
|
||||||
@@ -137,7 +147,22 @@ export const LoginPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
)}
|
)}
|
||||||
{configuredProviders.includes("generic") && (
|
{oauthProviders.includes("tailscale") && (
|
||||||
|
<Grid.Col span="content">
|
||||||
|
<Button
|
||||||
|
radius="xl"
|
||||||
|
leftSection={
|
||||||
|
<TailscaleIcon style={{ width: 14, height: 14 }} />
|
||||||
|
}
|
||||||
|
variant="default"
|
||||||
|
onClick={() => loginOAuthMutation.mutate("tailscale")}
|
||||||
|
loading={loginOAuthMutation.isLoading}
|
||||||
|
>
|
||||||
|
Tailscale
|
||||||
|
</Button>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
{oauthProviders.includes("generic") && (
|
||||||
<Grid.Col span="content">
|
<Grid.Col span="content">
|
||||||
<Button
|
<Button
|
||||||
radius="xl"
|
radius="xl"
|
||||||
@@ -153,21 +178,24 @@ export const LoginPage = () => {
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{configuredProviders.includes("username") && (
|
||||||
<Divider
|
<Divider
|
||||||
label="Or continue with email"
|
label="Or continue with password"
|
||||||
labelPosition="center"
|
labelPosition="center"
|
||||||
my="lg"
|
my="lg"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{configuredProviders.includes("username") && (
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email"
|
label="Username"
|
||||||
placeholder="user@example.com"
|
placeholder="user@example.com"
|
||||||
required
|
required
|
||||||
disabled={loginMutation.isLoading}
|
disabled={loginMutation.isLoading}
|
||||||
key={form.key("email")}
|
key={form.key("username")}
|
||||||
{...form.getInputProps("email")}
|
{...form.getInputProps("username")}
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label="Password"
|
||||||
@@ -187,6 +215,7 @@ export const LoginPage = () => {
|
|||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Layout } from "../components/layouts/layout";
|
|||||||
import { capitalize } from "../utils/utils";
|
import { capitalize } from "../utils/utils";
|
||||||
|
|
||||||
export const LogoutPage = () => {
|
export const LogoutPage = () => {
|
||||||
const { isLoggedIn, email, oauth, provider } = useUserContext();
|
const { isLoggedIn, username, oauth, provider } = useUserContext();
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return <Navigate to="/login" />;
|
return <Navigate to="/login" />;
|
||||||
@@ -32,7 +32,7 @@ export const LogoutPage = () => {
|
|||||||
color: "green",
|
color: "green",
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.replace("/login");
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -44,9 +44,9 @@ export const LogoutPage = () => {
|
|||||||
Logout
|
Logout
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
You are currently logged in as <Code>{email}</Code>
|
You are currently logged in as <Code>{username}</Code>
|
||||||
{oauth && ` using ${capitalize(provider)}`}. Click the button below to
|
{oauth && ` using ${capitalize(provider)} OAuth`}. Click the button
|
||||||
log out.
|
below to log out.
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import { Button, Code, Paper, Text } from "@mantine/core";
|
import { Button, Code, Paper, Text } from "@mantine/core";
|
||||||
import { Layout } from "../components/layouts/layout";
|
import { Layout } from "../components/layouts/layout";
|
||||||
import { useUserContext } from "../context/user-context";
|
|
||||||
import { Navigate } from "react-router";
|
import { Navigate } from "react-router";
|
||||||
|
|
||||||
export const UnauthorizedPage = () => {
|
export const UnauthorizedPage = () => {
|
||||||
const queryString = window.location.search;
|
const queryString = window.location.search;
|
||||||
const params = new URLSearchParams(queryString);
|
const params = new URLSearchParams(queryString);
|
||||||
const email = params.get("email");
|
const username = params.get("username");
|
||||||
|
const resource = params.get("resource");
|
||||||
|
|
||||||
const { isLoggedIn } = useUserContext();
|
if (username === "null") {
|
||||||
|
|
||||||
if (isLoggedIn) {
|
|
||||||
return <Navigate to="/" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (email === "null") {
|
|
||||||
return <Navigate to="/" />;
|
return <Navigate to="/" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,8 +19,14 @@ export const UnauthorizedPage = () => {
|
|||||||
Unauthorized
|
Unauthorized
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
The user with email address <Code>{email}</Code> is not authorized to
|
The user with username <Code>{username}</Code> is not authorized to{" "}
|
||||||
login.
|
{resource !== "null" ? (
|
||||||
|
<span>
|
||||||
|
access the <Code>{resource}</Code> resource.
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
"login."
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const userContextSchema = z.object({
|
export const userContextSchema = z.object({
|
||||||
isLoggedIn: z.boolean(),
|
isLoggedIn: z.boolean(),
|
||||||
email: z.string(),
|
username: z.string(),
|
||||||
oauth: z.boolean(),
|
oauth: z.boolean(),
|
||||||
provider: z.string(),
|
provider: z.string(),
|
||||||
configuredProviders: z.array(z.string()),
|
configuredProviders: z.array(z.string()),
|
||||||
|
|||||||
Reference in New Issue
Block a user