mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-09 03:30:18 +00:00
refactor: use some colors in CLI output (#962)
This commit is contained in:
+11
-5
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
@@ -11,15 +11,21 @@ import (
|
||||
func configCmd(tconfig *model.Config, loaders []cli.ResourceLoader) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "config",
|
||||
Description: "Print the configuration of Tinyauth",
|
||||
Description: "Dump the current configuration in YAML format, useful for debugging",
|
||||
Configuration: tconfig,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
jsonBytes, err := json.MarshalIndent(tconfig, "", " ")
|
||||
buf := strings.Builder{}
|
||||
|
||||
fmt.Fprint(&buf, "Your current configuration in YAML is:\n\n")
|
||||
|
||||
err := renderYamlToBuf(&buf, tconfig)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal configuration: %w", err)
|
||||
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
|
||||
fmt.Print(buf.String())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
)
|
||||
|
||||
func createOidcClientCmd() *cli.Command {
|
||||
@@ -31,40 +32,84 @@ func createOidcClientCmd() *cli.Command {
|
||||
return errors.New("client name can only contain alphanumeric characters and hyphens")
|
||||
}
|
||||
|
||||
uuid := uuid.New()
|
||||
clientId := uuid.String()
|
||||
u := uuid.New()
|
||||
clientId := u.String()
|
||||
clientSecret := "ta-" + utils.GenerateString(61)
|
||||
|
||||
uclientName := strings.ToUpper(clientName)
|
||||
lclientName := strings.ToLower(clientName)
|
||||
|
||||
builder := strings.Builder{}
|
||||
buf := strings.Builder{}
|
||||
|
||||
// header
|
||||
fmt.Fprintf(&builder, "Created credentials for client %s\n\n", clientName)
|
||||
fmt.Fprintf(&buf, "Created '%s' OIDC client.\n\n", clientName)
|
||||
|
||||
// credentials
|
||||
fmt.Fprintf(&builder, "Client Name: %s\n", clientName)
|
||||
fmt.Fprintf(&builder, "Client ID: %s\n", clientId)
|
||||
fmt.Fprintf(&builder, "Client Secret: %s\n\n", clientSecret)
|
||||
fmt.Fprintf(&buf, "Credentials:\n\n")
|
||||
fmt.Fprintf(&buf, "Client Name: %s\n", clientName)
|
||||
fmt.Fprintf(&buf, "Client ID: %s\n", clientId)
|
||||
fmt.Fprintf(&buf, "Client Secret: %s\n\n", clientSecret)
|
||||
|
||||
// env variables
|
||||
fmt.Fprint(&builder, "Environment variables:\n\n")
|
||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTID=%s\n", uclientName, clientId)
|
||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTSECRET=%s\n", uclientName, clientSecret)
|
||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_NAME=%s\n\n", uclientName, utils.Capitalize(lclientName))
|
||||
// end variables
|
||||
fmt.Fprintf(&buf, "Environment variables:\n\n")
|
||||
renderToBuf(&buf, []kv{
|
||||
{
|
||||
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_CLIENTID", uclientName),
|
||||
v: clientId,
|
||||
},
|
||||
{
|
||||
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_CLIENTSECRET", uclientName),
|
||||
v: clientSecret,
|
||||
},
|
||||
{
|
||||
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_NAME", uclientName),
|
||||
v: utils.Capitalize(lclientName),
|
||||
},
|
||||
}, "=")
|
||||
fmt.Fprintf(&buf, "\n")
|
||||
|
||||
// cli flags
|
||||
fmt.Fprint(&builder, "CLI flags:\n\n")
|
||||
fmt.Fprintf(&builder, "--oidc.clients.%s.clientid=%s\n", lclientName, clientId)
|
||||
fmt.Fprintf(&builder, "--oidc.clients.%s.clientsecret=%s\n", lclientName, clientSecret)
|
||||
fmt.Fprintf(&builder, "--oidc.clients.%s.name=%s\n\n", lclientName, utils.Capitalize(lclientName))
|
||||
fmt.Fprintf(&buf, "CLI flags:\n\n")
|
||||
renderToBuf(&buf, []kv{
|
||||
{
|
||||
k: fmt.Sprintf("--oidc.clients.%s.clientid", lclientName),
|
||||
v: clientId,
|
||||
},
|
||||
{
|
||||
k: fmt.Sprintf("--oidc.clients.%s.clientsecret", lclientName),
|
||||
v: clientSecret,
|
||||
},
|
||||
{
|
||||
k: fmt.Sprintf("--oidc.clients.%s.name", lclientName),
|
||||
v: utils.Capitalize(lclientName),
|
||||
},
|
||||
}, "=")
|
||||
fmt.Fprintf(&buf, "\n")
|
||||
|
||||
// yaml config
|
||||
fmt.Fprintf(&buf, "YAML config:\n\n")
|
||||
|
||||
err = renderYamlToBuf(&buf, &model.OIDCConfig{
|
||||
Clients: map[string]model.OIDCClientConfig{
|
||||
lclientName: {
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
Name: utils.Capitalize(lclientName),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
|
||||
// footer
|
||||
fmt.Fprintln(&builder, "You can use either option to configure your OIDC client. Make sure to save these credentials as there is no way to regenerate them.")
|
||||
fmt.Fprintln(&buf, "You can use any of the above options to configure your OIDC client. Make sure to save these credentials as there is no way to regenerate them.")
|
||||
|
||||
// print
|
||||
out := builder.String()
|
||||
out := buf.String()
|
||||
fmt.Print(out)
|
||||
return nil
|
||||
},
|
||||
|
||||
+100
-54
@@ -3,11 +3,12 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -34,62 +35,107 @@ func createUserCmd() *cli.Command {
|
||||
&cli.FlagLoader{},
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
cmd := &cli.Command{
|
||||
Name: "create",
|
||||
Description: "Create a user",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log := logger.NewLogger().WithSimpleConfig()
|
||||
log.Init()
|
||||
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("username cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("password cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&tCfg.Docker),
|
||||
),
|
||||
)
|
||||
|
||||
theme := new(themeBase)
|
||||
err := form.WithTheme(theme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tCfg.Username == "" || tCfg.Password == "" {
|
||||
return errors.New("username and password cannot be empty")
|
||||
}
|
||||
|
||||
log.App.Info().Str("username", tCfg.Username).Msg("Creating user")
|
||||
|
||||
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// If docker format is enabled, escape the dollar sign
|
||||
passwdStr := string(passwd)
|
||||
if tCfg.Docker {
|
||||
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||
}
|
||||
|
||||
log.App.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Run = func(_ []string) error {
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("username cannot be empty")
|
||||
}
|
||||
if strings.Contains(s, ":") {
|
||||
return errors.New("username cannot contain ':'")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("password cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&tCfg.Docker),
|
||||
),
|
||||
)
|
||||
|
||||
theme := new(themeBase)
|
||||
|
||||
err := form.WithTheme(theme).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tCfg.Username == "" || tCfg.Password == "" {
|
||||
cmd.PrintHelp(os.Stdout)
|
||||
return errors.New("username and password cannot be empty")
|
||||
}
|
||||
|
||||
if strings.Contains(tCfg.Username, ":") {
|
||||
return errors.New("username cannot contain ':'")
|
||||
}
|
||||
|
||||
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// Only the docker compose output needs $ escaped, the raw hash is correct everywhere else
|
||||
passwdStr := string(passwd)
|
||||
outputStr := passwdStr
|
||||
|
||||
if tCfg.Docker {
|
||||
outputStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||
}
|
||||
|
||||
user := fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)
|
||||
escapedUser := fmt.Sprintf("%s:%s", tCfg.Username, outputStr)
|
||||
|
||||
buf := strings.Builder{}
|
||||
|
||||
// header
|
||||
fmt.Fprintf(&buf, "Created user '%s'.\n\n", tCfg.Username)
|
||||
|
||||
// environment variable
|
||||
fmt.Fprint(&buf, "Environment variable:\n\n")
|
||||
renderToBuf(&buf, []kv{
|
||||
{"TINYAUTH_AUTH_USERS", escapedUser},
|
||||
}, "=")
|
||||
|
||||
// cli flags
|
||||
fmt.Fprint(&buf, "\nCLI flags:\n\n")
|
||||
renderToBuf(&buf, []kv{
|
||||
{"--auth.users", user},
|
||||
}, "=")
|
||||
|
||||
// yaml config
|
||||
fmt.Fprint(&buf, "\nYAML config:\n\n")
|
||||
|
||||
err = renderYamlToBuf(&buf, &model.Config{
|
||||
Auth: model.AuthConfig{
|
||||
Users: []string{user},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
|
||||
// footer
|
||||
fmt.Fprint(&buf, "Use your config option of choice to add the user to Tinyauth and then restart.")
|
||||
|
||||
fmt.Println(buf.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/mdp/qrterminal/v3"
|
||||
@@ -34,85 +33,98 @@ func generateTotpCmd() *cli.Command {
|
||||
&cli.FlagLoader{},
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
cmd := &cli.Command{
|
||||
Name: "generate",
|
||||
Description: "Generate a TOTP secret",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log := logger.NewLogger().WithSimpleConfig()
|
||||
log.Init()
|
||||
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Current user (username:hash)").Value(&tCfg.User).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("user cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
theme := new(themeBase)
|
||||
err := form.WithTheme(theme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(tCfg.User)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse user: %w", err)
|
||||
}
|
||||
|
||||
docker := false
|
||||
if strings.Contains(tCfg.User, "$$") {
|
||||
docker = true
|
||||
}
|
||||
|
||||
if user.TOTPSecret != "" {
|
||||
return fmt.Errorf("user already has a TOTP secret")
|
||||
}
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "Tinyauth",
|
||||
AccountName: user.Username,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate TOTP secret: %w", err)
|
||||
}
|
||||
|
||||
secret := key.Secret()
|
||||
|
||||
log.App.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
||||
|
||||
log.App.Info().Msg("Generated QR code")
|
||||
|
||||
config := qrterminal.Config{
|
||||
Level: qrterminal.L,
|
||||
Writer: os.Stdout,
|
||||
BlackChar: qrterminal.BLACK,
|
||||
WhiteChar: qrterminal.WHITE,
|
||||
QuietZone: 2,
|
||||
}
|
||||
|
||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||
|
||||
user.TOTPSecret = secret
|
||||
|
||||
// If using docker escape re-escape it
|
||||
if docker {
|
||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||
}
|
||||
|
||||
log.App.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Run = func(_ []string) error {
|
||||
colors := getColors()
|
||||
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Current user (username:hash)").Value(&tCfg.User).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("user cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
theme := new(themeBase)
|
||||
err := form.WithTheme(theme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tCfg.User == "" {
|
||||
cmd.PrintHelp(os.Stdout)
|
||||
return fmt.Errorf("user is required")
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(tCfg.User)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse user: %w", err)
|
||||
}
|
||||
|
||||
docker := false
|
||||
if strings.Contains(tCfg.User, "$$") {
|
||||
docker = true
|
||||
}
|
||||
|
||||
if user.TOTPSecret != "" {
|
||||
return fmt.Errorf("user already has a TOTP secret")
|
||||
}
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "Tinyauth",
|
||||
AccountName: user.Username,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate TOTP secret: %w", err)
|
||||
}
|
||||
|
||||
secret := key.Secret()
|
||||
|
||||
fmt.Printf("Scan the following QR code with your authenticator app (e.g., Google Authenticator, 2fauth, Microsoft Authenticator):\n\n")
|
||||
|
||||
config := qrterminal.Config{
|
||||
Level: qrterminal.L,
|
||||
Writer: os.Stdout,
|
||||
BlackChar: qrterminal.BLACK,
|
||||
WhiteChar: qrterminal.WHITE,
|
||||
QuietZone: 2,
|
||||
}
|
||||
|
||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||
|
||||
user.TOTPSecret = secret
|
||||
|
||||
// If using docker escape re-escape it
|
||||
if docker {
|
||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||
}
|
||||
|
||||
userStr := fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)
|
||||
|
||||
fmt.Print("\nOr add the following TOTP secret to your authenticator app: ")
|
||||
fmt.Print(colors.green.Render(secret))
|
||||
fmt.Print("\n\n")
|
||||
|
||||
fmt.Printf("Finally, add your user '%s' back to your configuration: ", user.Username)
|
||||
fmt.Print(colors.green.Render(userStr))
|
||||
fmt.Print("\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+135
-16
@@ -2,13 +2,16 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/loaders"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/tinyauthapp/paerser/cli"
|
||||
)
|
||||
|
||||
@@ -34,83 +37,104 @@ func main() {
|
||||
|
||||
cmdUser := &cli.Command{
|
||||
Name: "user",
|
||||
Description: "Manage Tinyauth users",
|
||||
Description: "Manage users",
|
||||
}
|
||||
|
||||
cmdTotp := &cli.Command{
|
||||
Name: "totp",
|
||||
Description: "Manage Tinyauth TOTP users",
|
||||
Description: "Manage TOTP users",
|
||||
}
|
||||
|
||||
cmdOidc := &cli.Command{
|
||||
Name: "oidc",
|
||||
Description: "Manage Tinyauth OIDC clients",
|
||||
Description: "Manage OIDC clients",
|
||||
}
|
||||
|
||||
err := cmdTinyauth.AddCommand(versionCmd())
|
||||
helpCmd := &cli.Command{
|
||||
Name: "help",
|
||||
Description: "Show the help message",
|
||||
Run: func(_ []string) error {
|
||||
return cmdTinyauth.PrintHelp(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
err := cmdTinyauth.AddCommand(helpCmd)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add version command")
|
||||
fatalf(err, "Failed to add help command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(versionCmd())
|
||||
|
||||
if err != nil {
|
||||
fatalf(err, "Failed to add version command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(configCmd(tConfig, loaders))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add config command")
|
||||
fatalf(err, "Failed to add config command")
|
||||
}
|
||||
|
||||
err = cmdUser.AddCommand(verifyUserCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add verify command")
|
||||
fatalf(err, "Failed to add user verify command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(healthcheckCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add healthcheck command")
|
||||
fatalf(err, "Failed to add healthcheck command")
|
||||
}
|
||||
|
||||
err = cmdTotp.AddCommand(generateTotpCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add generate command")
|
||||
fatalf(err, "Failed to add totp generate command")
|
||||
}
|
||||
|
||||
err = cmdUser.AddCommand(createUserCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
||||
fatalf(err, "Failed to add create user command")
|
||||
}
|
||||
|
||||
err = cmdOidc.AddCommand(createOidcClientCmd())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
||||
fatalf(err, "Failed to add create oidc client command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(cmdUser)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add user command")
|
||||
fatalf(err, "Failed to add user command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(cmdTotp)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add totp command")
|
||||
fatalf(err, "Failed to add totp command")
|
||||
}
|
||||
|
||||
err = cmdTinyauth.AddCommand(cmdOidc)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to add oidc command")
|
||||
fatalf(err, "Failed to add oidc command")
|
||||
}
|
||||
|
||||
err = cli.Execute(cmdTinyauth)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to execute command")
|
||||
if strings.Contains(err.Error(), "command not found") {
|
||||
fmt.Println("Command not found. Use 'tinyauth help' to see available commands.")
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "is not runnable") {
|
||||
return
|
||||
}
|
||||
fatalf(err, "Failed to execute command")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,3 +155,98 @@ type themeBase struct{}
|
||||
func (t *themeBase) Theme(isDark bool) *huh.Styles {
|
||||
return huh.ThemeBase(isDark)
|
||||
}
|
||||
|
||||
type colors struct {
|
||||
blue lipgloss.Style
|
||||
gray lipgloss.Style
|
||||
lightGray lipgloss.Style
|
||||
green lipgloss.Style
|
||||
yellow lipgloss.Style
|
||||
}
|
||||
|
||||
func getColors() colors {
|
||||
noColor := os.Getenv("NO_COLOR")
|
||||
forceColor := os.Getenv("FORCE_COLOR")
|
||||
|
||||
colorOut := colors{
|
||||
green: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(34)),
|
||||
gray: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(245)),
|
||||
yellow: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(214)),
|
||||
blue: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(75)),
|
||||
lightGray: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(250)),
|
||||
}
|
||||
|
||||
noColorOut := colors{
|
||||
green: lipgloss.NewStyle(),
|
||||
gray: lipgloss.NewStyle(),
|
||||
yellow: lipgloss.NewStyle(),
|
||||
blue: lipgloss.NewStyle(),
|
||||
lightGray: lipgloss.NewStyle(),
|
||||
}
|
||||
|
||||
useColors := true
|
||||
|
||||
if noColor == "true" || noColor == "1" {
|
||||
useColors = false
|
||||
}
|
||||
|
||||
if forceColor == "true" || forceColor == "1" {
|
||||
useColors = true
|
||||
}
|
||||
|
||||
if !useColors {
|
||||
return noColorOut
|
||||
}
|
||||
|
||||
return colorOut
|
||||
}
|
||||
|
||||
func fatalf(err error, msg string) {
|
||||
fmt.Printf("%s: %v\n", msg, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
type kv struct {
|
||||
k string
|
||||
v string
|
||||
}
|
||||
|
||||
func renderToBuf(buf *strings.Builder, kv []kv, sep string) {
|
||||
colors := getColors()
|
||||
for _, i := range kv {
|
||||
buf.WriteString(colors.blue.Render(i.k))
|
||||
buf.WriteString(colors.gray.Render(sep))
|
||||
buf.WriteString(colors.lightGray.Render(i.v))
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func renderYamlToBuf(buf *strings.Builder, i any) error {
|
||||
colors := getColors()
|
||||
|
||||
yout, err := yaml.Marshal(i)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal yaml: %w", err)
|
||||
}
|
||||
|
||||
for l := range strings.SplitSeq(string(yout), "\n") {
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimLeft(l, " "), "- ") {
|
||||
buf.WriteString(colors.lightGray.Render(l))
|
||||
buf.WriteString("\n")
|
||||
continue
|
||||
}
|
||||
lp := strings.SplitN(l, ":", 2)
|
||||
buf.WriteString(colors.blue.Render(lp[0]))
|
||||
buf.WriteString(colors.gray.Render(":"))
|
||||
if len(lp) == 2 {
|
||||
buf.WriteString(colors.lightGray.Render(lp[1]))
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+79
-73
@@ -3,9 +3,9 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/pquerna/otp/totp"
|
||||
@@ -38,81 +38,87 @@ func verifyUserCmd() *cli.Command {
|
||||
&cli.FlagLoader{},
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
cmd := &cli.Command{
|
||||
Name: "verify",
|
||||
Description: "Verify a user is set up correctly",
|
||||
Configuration: tCfg,
|
||||
Resources: loaders,
|
||||
Run: func(_ []string) error {
|
||||
log := logger.NewLogger().WithSimpleConfig()
|
||||
log.Init()
|
||||
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("User (username:hash:totp)").Value(&tCfg.User).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("user cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("username cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("password cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("TOTP Code (optional)").Value(&tCfg.Totp),
|
||||
),
|
||||
)
|
||||
|
||||
theme := new(themeBase)
|
||||
err := form.WithTheme(theme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(tCfg.User)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse user: %w", err)
|
||||
}
|
||||
|
||||
if user.Username != tCfg.Username {
|
||||
return fmt.Errorf("username is incorrect")
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(tCfg.Password))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("password is incorrect: %w", err)
|
||||
}
|
||||
|
||||
if user.TOTPSecret == "" {
|
||||
if tCfg.Totp != "" {
|
||||
log.App.Warn().Msg("User does not have TOTP secret")
|
||||
}
|
||||
log.App.Info().Msg("User verified")
|
||||
return nil
|
||||
}
|
||||
|
||||
ok := totp.Validate(tCfg.Totp, user.TOTPSecret)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("TOTP code incorrect")
|
||||
}
|
||||
|
||||
log.App.Info().Msg("User verified")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Run = func(_ []string) error {
|
||||
colors := getColors()
|
||||
|
||||
if tCfg.Interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("User (username:hash:totp)").Value(&tCfg.User).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("user cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("username cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("password cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("TOTP Code (optional)").Value(&tCfg.Totp),
|
||||
),
|
||||
)
|
||||
|
||||
theme := new(themeBase)
|
||||
err := form.WithTheme(theme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tCfg.User == "" || tCfg.Username == "" || tCfg.Password == "" {
|
||||
cmd.PrintHelp(os.Stdout)
|
||||
return fmt.Errorf("user, username, and password are required")
|
||||
}
|
||||
|
||||
user, err := utils.ParseUser(tCfg.User)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse user: %w", err)
|
||||
}
|
||||
|
||||
if user.Username != tCfg.Username {
|
||||
return fmt.Errorf("username is incorrect")
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(tCfg.Password))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("password is incorrect: %w", err)
|
||||
}
|
||||
|
||||
if user.TOTPSecret == "" {
|
||||
if tCfg.Totp != "" {
|
||||
fmt.Println(colors.yellow.Render("⚠") + " TOTP code provided but user does not have TOTP enabled")
|
||||
}
|
||||
fmt.Println(colors.green.Render("✓") + " User verified")
|
||||
return nil
|
||||
}
|
||||
|
||||
ok := totp.Validate(tCfg.Totp, user.TOTPSecret)
|
||||
if !ok {
|
||||
return fmt.Errorf("TOTP code incorrect")
|
||||
}
|
||||
|
||||
fmt.Println(colors.green.Render("✓") + " User verified")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ func versionCmd() *cli.Command {
|
||||
Configuration: nil,
|
||||
Resources: nil,
|
||||
Run: func(_ []string) error {
|
||||
fmt.Printf("Version: %s\n", model.Version)
|
||||
fmt.Printf("Commit Hash: %s\n", model.CommitHash)
|
||||
fmt.Printf("Build Timestamp: %s\n", model.BuildTimestamp)
|
||||
colors := getColors()
|
||||
fmt.Printf("Version: %s\n", colors.blue.Render(model.Version))
|
||||
fmt.Printf("Commit Hash: %s\n", colors.blue.Render(model.CommitHash))
|
||||
fmt.Printf("Build Timestamp: %s\n", colors.blue.Render(model.BuildTimestamp))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user