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()) }