commit 51a229d43c281b80cad12317b278402c69e7ed31 Author: Torjus HÃ¥kestad Date: Mon Oct 23 21:27:11 2023 +0200 Initial commit diff --git a/actions/auth.go b/actions/auth.go new file mode 100644 index 0000000..f5fc453 --- /dev/null +++ b/actions/auth.go @@ -0,0 +1,199 @@ +package actions + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "syscall" + "time" + + "git.t-juice.club/microfilm/auth" + "github.com/golang-jwt/jwt/v5" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/urfave/cli/v2" + "golang.org/x/term" +) + +func Login(cCtx *cli.Context) error { + username, password, err := getCreds() + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(cCtx.Context, 15*time.Second) + defer cancel() + + body := jsonToReader(&auth.TokenRequest{ + Password: password, + }) + + url := fmt.Sprintf("http://localhost:8085/auth/%s/token", username) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusUnauthorized { + return fmt.Errorf("Invalid username/password.") + } + + var errData auth.ErrorResponse + decoder := json.NewDecoder(resp.Body) + if err := decoder.Decode(&errData); err != nil { + return fmt.Errorf("unable to decode error: %w", err) + } + + return fmt.Errorf("error %d when authenticating: %s", errData.Status, errData.Message) + } + + var responseData auth.TokenResponse + + decoder := json.NewDecoder(resp.Body) + if err := decoder.Decode(&responseData); err != nil { + return err + } + + if err := SaveToken(responseData.Token); err != nil { + return err + } + + if cCtx.Bool("claims") { + claims, err := ClaimsFromToken(responseData.Token) + if err != nil { + return err + } + printClaims(claims) + + } + + return nil +} + +func getCreds() (string, string, error) { + reader := bufio.NewReader(os.Stdin) + + fmt.Print("Enter Username: ") + username, err := reader.ReadString('\n') + if err != nil { + return "", "", err + } + + fmt.Print("Enter Password: ") + bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", "", err + } + + password := string(bytePassword) + fmt.Printf("\n") + return strings.TrimSpace(username), strings.TrimSpace(password), nil +} + +func jsonToReader(data any) io.Reader { + var buf bytes.Buffer + encoder := json.NewEncoder(&buf) + if err := encoder.Encode(data); err != nil { + panic(err) + } + + return &buf +} + +func SaveToken(token string) error { + configBaseDir, err := os.UserCacheDir() + if err != nil { + return err + } + configDir := filepath.Join(configBaseDir, "microfilm") + if err := os.MkdirAll(configDir, os.ModePerm); err != nil { + return err + } + + configFilePath := filepath.Join(configDir, "token.json") + + f, err := os.Create(configFilePath) + if err != nil { + return err + } + defer f.Close() + + encoder := json.NewEncoder(f) + if err := encoder.Encode(token); err != nil { + return err + } + + return nil +} + +func LoadToken() (string, error) { + configBaseDir, err := os.UserCacheDir() + if err != nil { + return "", err + } + configFilePath := filepath.Join(configBaseDir, "microfilm", "token.json") + + f, err := os.Open(configFilePath) + if err != nil { + return "", err + } + defer f.Close() + + var token string + decoder := json.NewDecoder(f) + if err := decoder.Decode(&token); err != nil { + return "", err + } + + return token, nil +} + +func ClaimsFromToken(tokenString string) (*auth.MicrofilmClaims, error) { + parser := jwt.NewParser() + token, _, err := parser.ParseUnverified(tokenString, &auth.MicrofilmClaims{}) + if err != nil { + return nil, err + } + + claims := token.Claims.(*auth.MicrofilmClaims) + return claims, nil +} + +func claimsToMap(claims *auth.MicrofilmClaims) map[string]string { + m := make(map[string]string) + + m["role"] = claims.Role + m["sub"] = claims.Subject + m["aud"] = strings.Join(claims.Audience, ",") + m["jti"] = claims.ID + m["iat"] = claims.IssuedAt.String() + m["nbf"] = claims.NotBefore.String() + m["exp"] = claims.ExpiresAt.String() + m["iss"] = claims.Issuer + + return m +} + +func printClaims(claims *auth.MicrofilmClaims) { + tw := table.NewWriter() + + tw.AppendHeader(table.Row{"Field", "Value"}) + for k, v := range claimsToMap(claims) { + tw.AppendRow(table.Row{k, v}) + } + + fmt.Println(tw.Render()) +} diff --git a/actions/user.go b/actions/user.go new file mode 100644 index 0000000..cb8e9b2 --- /dev/null +++ b/actions/user.go @@ -0,0 +1,67 @@ +package actions + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "git.t-juice.club/microfilm/users" + "github.com/urfave/cli/v2" +) + +func UserCreate(c *cli.Context) error { + token, err := LoadToken() + if err != nil { + return fmt.Errorf("Unable to load token: %w", err) + } + + username, password, err := getCreds() + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(c.Context, 5*time.Second) + defer cancel() + + body := jsonToReader(&users.CreateUserRequest{ + Username: username, + Password: password, + }) + + url := "http://localhost:8085/user/" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) + if err != nil { + return err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var errData users.ErrorResponse + decoder := json.NewDecoder(resp.Body) + if err := decoder.Decode(&errData); err != nil { + return fmt.Errorf("unable to decode error: %w", err) + } + + return fmt.Errorf("error %d when authenticating: %s", errData.Status, errData.Message) + } + + var responseData users.CreateUserResponse + + decoder := json.NewDecoder(resp.Body) + if err := decoder.Decode(&responseData); err != nil { + return err + } + + fmt.Printf("Created user with id %s", responseData.User.ID) + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f09bfa5 --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module git.t-juice.club/microfilm/cli + +go 1.21.3 + +require ( + git.t-juice.club/microfilm/auth v0.0.0-20231021082615-84499d0ed9d4 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/jedib0t/go-pretty/v6 v6.4.8 + github.com/urfave/cli/v2 v2.25.7 + golang.org/x/term v0.13.0 +) + +require github.com/stretchr/testify v1.8.4 // indirect + +require ( + git.t-juice.club/microfilm/users v0.1.0 + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/sys v0.13.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b3b3e2b --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +git.t-juice.club/microfilm/auth v0.0.0-20231021082615-84499d0ed9d4 h1:sJb64fKll43D/wVj0Xir2TPXhkY4f8BzlREbzD+pDCY= +git.t-juice.club/microfilm/auth v0.0.0-20231021082615-84499d0ed9d4/go.mod h1:sfgaIWxnNgERWyx611596OtEBc3cF4g3FSqKd073Te4= +git.t-juice.club/microfilm/users v0.1.0 h1:UszaeXVQKVOmWK8zR502rGvDU6D7MJS6MgObn9Tfce4= +git.t-juice.club/microfilm/users v0.1.0/go.mod h1:CZRF0zaNCCwMDHVZyubUcotncZsHTlrIA5ngdflzO58= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/jedib0t/go-pretty/v6 v6.4.8 h1:HiNzyMSEpsBaduKhmK+CwcpulEeBrTmxutz4oX/oWkg= +github.com/jedib0t/go-pretty/v6 v6.4.8/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6c6e3f1 --- /dev/null +++ b/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "os" + + "git.t-juice.club/microfilm/cli/actions" + "github.com/urfave/cli/v2" +) + +func main() { + app := cli.App{ + Name: "mf-cli", + Commands: []*cli.Command{ + { + Name: "auth", + Usage: "Auth-related commands.", + Action: func(ctx *cli.Context) error { + return cli.ShowSubcommandHelp(ctx) + }, + Subcommands: []*cli.Command{ + { + Name: "login", + Usage: "Login to microfilm. Storing token.", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "claims", + Usage: "Print claims of received token.", + }, + }, + Action: actions.Login, + }, + }, + }, + { + Name: "user", + Usage: "User-related commands.", + Action: func(ctx *cli.Context) error { + return cli.ShowSubcommandHelp(ctx) + }, + Subcommands: []*cli.Command{ + { + Name: "create", + Usage: "Create a new user.", + Action: actions.UserCreate, + }, + }, + }, + }, + Action: cli.ShowAppHelp, + } + + if err := app.Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "Error: %s", err) + } +}