chore: Adds login and user creation support to the API.

This commit is contained in:
Tera << 8 2025-01-05 20:51:06 -05:00
parent 96833b238b
commit 843cd34785
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
13 changed files with 683 additions and 12 deletions

View file

@ -0,0 +1,115 @@
package users
import (
"errors"
"fmt"
"os"
"syscall"
"git.terah.dev/imterah/hermes/apiclient"
"git.terah.dev/imterah/hermes/frontend/config"
"github.com/charmbracelet/log"
"github.com/urfave/cli/v2"
"golang.org/x/term"
"gopkg.in/yaml.v3"
)
func CreateUserCommand(cCtx *cli.Context) error {
configPath := cCtx.String("config-path")
var configContents *config.Config
_, err := os.Stat(configPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
configContents = &config.Config{}
} else {
return fmt.Errorf("failed to get configuration file information: %s", err.Error())
}
} else {
configContents, err = config.ReadAndParseConfig(configPath)
if err != nil {
return fmt.Errorf("failed to read and parse configuration file: %s", err.Error())
}
}
username := cCtx.String("username")
if username == "" {
if configContents.Username == "" {
return fmt.Errorf("username not specified and username is not in the configuration file")
}
username = configContents.Username
}
var password string
if cCtx.Bool("ask-password") {
fmt.Print("Password: ")
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
fmt.Print("\n")
if err != nil {
return fmt.Errorf("failed to read password from console: %s", err.Error())
}
password = string(passwordBytes)
} else {
password = cCtx.String("password")
if password == "" {
return fmt.Errorf("password is not specified and password asking is not enabled")
}
}
var serverURL string
if cCtx.String("server-url") == "" {
if configContents.APIPath == "" {
return fmt.Errorf("server URL not specified and server URL is not in the configuration file")
}
serverURL = configContents.APIPath
} else {
serverURL = cCtx.String("server-url")
}
fullName := cCtx.String("full-name")
email := cCtx.String("email")
isBot := cCtx.Bool("user-is-bot")
log.Info("Creating user...")
api := &apiclient.HermesAPIClient{
URL: serverURL,
}
refreshToken, err := api.UserCreate(fullName, username, email, password, isBot)
if err != nil {
return fmt.Errorf("failed to create user: %s", err.Error())
}
log.Info("Successfully created user.")
if cCtx.Bool("do-not-save-configuration") {
return nil
}
configContents.Username = username
configContents.RefreshToken = refreshToken
configContents.APIPath = serverURL
data, err := yaml.Marshal(configContents)
if err != nil {
return fmt.Errorf("failed to marshal configuration data: %s", err.Error())
}
os.WriteFile(configPath, data, 0644)
return nil
}

View file

@ -0,0 +1,98 @@
package users
import (
"errors"
"fmt"
"os"
"syscall"
"git.terah.dev/imterah/hermes/apiclient"
"git.terah.dev/imterah/hermes/frontend/config"
"github.com/charmbracelet/log"
"github.com/urfave/cli/v2"
"golang.org/x/term"
"gopkg.in/yaml.v3"
)
func GetRefreshTokenCommand(cCtx *cli.Context) error {
configPath := cCtx.String("config-path")
var configContents *config.Config
_, err := os.Stat(configPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
configContents = &config.Config{}
} else {
return fmt.Errorf("failed to get configuration file information: %s", err.Error())
}
} else {
configContents, err = config.ReadAndParseConfig(configPath)
if err != nil {
return fmt.Errorf("failed to read and parse configuration file: %s", err.Error())
}
}
var username string
var password string
if cCtx.String("username") == "" {
if configContents.Username == "" {
return fmt.Errorf("username not specified and username is not in the configuration file")
}
username = configContents.Username
} else {
username = cCtx.String("username")
}
if cCtx.Bool("ask-password") {
fmt.Print("Password: ")
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
fmt.Print("\n")
if err != nil {
return fmt.Errorf("failed to read password from console: %s", err.Error())
}
password = string(passwordBytes)
} else {
password = cCtx.String("password")
if password == "" {
return fmt.Errorf("password is not specified and password asking is not enabled")
}
}
serverURL := cCtx.String("server-url")
log.Info("Authenticating with API...")
api := &apiclient.HermesAPIClient{
URL: serverURL,
}
refreshToken, err := api.UserGetRefreshToken(&username, nil, password)
if err != nil {
return fmt.Errorf("failed to authenticate with the API: %s", err.Error())
}
configContents.Username = username
configContents.RefreshToken = refreshToken
configContents.APIPath = serverURL
log.Info("Writing configuration file...")
data, err := yaml.Marshal(configContents)
if err != nil {
return fmt.Errorf("failed to marshal configuration data: %s", err.Error())
}
log.Infof("config path: %s", configPath)
os.WriteFile(configPath, data, 0644)
return nil
}

30
frontend/config/config.go Normal file
View file

@ -0,0 +1,30 @@
package config
import (
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
Username string `json:"username"`
RefreshToken string `json:"token"`
APIPath string `json:"api_path"`
}
func ReadAndParseConfig(configFile string) (*Config, error) {
configFileContents, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
config := &Config{}
err = yaml.Unmarshal(configFileContents, config)
if err != nil {
return nil, err
}
return config, nil
}

1
frontend/dev.env Normal file
View file

@ -0,0 +1 @@
HERMES_LOG_LEVEL=debug

143
frontend/main.go Normal file
View file

@ -0,0 +1,143 @@
package main
import (
"os"
"path"
"git.terah.dev/imterah/hermes/frontend/commands/users"
"github.com/charmbracelet/log"
"github.com/urfave/cli/v2"
)
func main() {
logLevel := os.Getenv("HERMES_LOG_LEVEL")
if logLevel != "" {
switch logLevel {
case "debug":
log.SetLevel(log.DebugLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "warn":
log.SetLevel(log.WarnLevel)
case "error":
log.SetLevel(log.ErrorLevel)
case "fatal":
log.SetLevel(log.FatalLevel)
}
}
configDir, err := os.UserConfigDir()
if err != nil {
log.Fatalf("Failed to get configuration directory: %s", err.Error())
}
app := &cli.App{
Name: "hermcli",
Usage: "client for Hermes -- port forwarding across boundaries",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config-path",
Aliases: []string{"config", "cp", "c"},
Value: path.Join(configDir, "hermcli.yml"),
},
},
Commands: []*cli.Command{
{
Name: "login",
Usage: "log in to the API",
Action: users.GetRefreshTokenCommand,
Aliases: []string{"l"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"user", "u"},
Usage: "username to authenticate as",
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"pass", "p"},
Usage: "password to authenticate with",
},
&cli.StringFlag{
Name: "server-url",
Aliases: []string{"server", "s"},
Usage: "URL of the server to authenticate with",
},
&cli.BoolFlag{
Name: "ask-password",
Aliases: []string{"ask-pass", "ap"},
Usage: "asks you the password to authenticate with",
},
},
},
{
Name: "users",
Usage: "user management commands",
Aliases: []string{"u"},
Subcommands: []*cli.Command{
{
Name: "create",
Aliases: []string{"c"},
Usage: "create a user",
Action: users.CreateUserCommand,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "full-name",
Aliases: []string{"name", "n"},
Usage: "full name for the user",
Required: true,
},
&cli.StringFlag{
Name: "username",
Aliases: []string{"user", "us"},
Usage: "username to give the user",
Required: true,
},
&cli.StringFlag{
Name: "email",
Aliases: []string{"e"},
Usage: "email to give the user",
Required: true,
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"pass", "p"},
Usage: "password to give the user",
},
&cli.StringFlag{
Name: "server-url",
Aliases: []string{"server", "s"},
Usage: "URL of the server to connect with",
},
&cli.BoolFlag{
Name: "ask-password",
Aliases: []string{"ask-pass", "ap"},
Usage: "asks you the password to give the user",
},
&cli.BoolFlag{
Name: "user-is-bot",
Aliases: []string{"user-bot", "ub", "u"},
Usage: "if set, makes the user flagged as a bot",
},
&cli.BoolFlag{
Name: "do-not-save-configuration",
Aliases: []string{"no-save", "ns"},
Usage: "doesn't save the authenticated user credentials",
},
},
},
},
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}