package cli import ( "fmt" "text/tabwriter" "time" "github.com/spf13/cobra" "gitea.mynodes.xyz/ilker/llmgw/internal/account" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) func newUsersCmd() *cobra.Command { var sf storeFlags cmd := &cobra.Command{ Use: "users", Short: "Approve and manage the people who sign up", Long: "Signing up creates an account that cannot do anything. Approval is what\n" + "makes it work, and it happens here rather than in a browser: approving is\n" + "the step that decides who gets to spend the cluster, and it needs a shell\n" + "on this machine rather than a session cookie.", } sf.bind(cmd) open := func(cmd *cobra.Command) (*store.DB, error) { return sf.open(cmd) } cmd.AddCommand(newUserListCmd(open), newUserApproveCmd(open), newUserStateCmd(open, "suspend", store.UserSuspended, "Stop an account working, keeping it and its history"), newUserStateCmd(open, "reinstate", store.UserActive, "Let a suspended account work again"), newUserCreateCmd(open), newUserResetPasswordCmd(open), newUserAdminCmd(open, "promote", true, "Let an account see the cluster dashboard at /admin"), newUserAdminCmd(open, "demote", false, "Take away access to the cluster dashboard")) return cmd } type userOpener func(*cobra.Command) (*store.DB, error) func newUserListCmd(open userOpener) *cobra.Command { var pending bool cmd := &cobra.Command{ Use: "list", Short: "List accounts", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { s, err := open(cmd) if err != nil { return err } defer s.Close() state := store.UserState("") if pending { state = store.UserPending } users, err := s.Users(cmd.Context(), state) if err != nil { return err } if len(users) == 0 { fmt.Fprintln(cmd.OutOrStdout(), "no accounts") return nil } w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tEMAIL\tNAME\tSTATE\tADMIN\tPROJECT\tSIGNED UP\tLAST LOGIN") now := time.Now() for _, u := range users { admin := "-" if u.IsAdmin { admin = "yes" } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", u.ID, u.Email, u.Name, u.State, admin, dash(u.ProjectID), ago(u.CreatedAt, now), ago(u.LastLoginAt, now)) } return w.Flush() }, } cmd.Flags().BoolVar(&pending, "pending", false, "only accounts waiting for approval") return cmd } func newUserApproveCmd(open userOpener) *cobra.Command { var project, by string cmd := &cobra.Command{ Use: "approve ", Short: "Let an account sign in and issue its own keys", Long: "The project is required: it decides which limits this person's keys\n" + "inherit. An account approved into no project would pass every check and\n" + "then fail at the first key it tried to make.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { s, err := open(cmd) if err != nil { return err } defer s.Close() if err := s.ApproveUser(cmd.Context(), args[0], project, by, time.Now()); err != nil { return fmt.Errorf("approving %s: %w", args[0], err) } u, err := s.UserByID(cmd.Context(), args[0]) if err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s approved into project %s\n", u.Email, project) cmd.PrintErrln("They can sign in and create their own keys from /profile.") return nil }, } cmd.Flags().StringVar(&project, "project", "default", "project the person's keys belong to") cmd.Flags().StringVar(&by, "by", "", "who approved, for the record") _ = cmd.MarkFlagRequired("project") return cmd } func newUserStateCmd(open userOpener, verb string, state store.UserState, short string) *cobra.Command { return &cobra.Command{ Use: verb + " ", Short: short, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { s, err := open(cmd) if err != nil { return err } defer s.Close() if err := s.SetUserState(cmd.Context(), args[0], state); err != nil { return err } if state == store.UserSuspended { // Otherwise the person stays signed in and suspension is // advisory until their session happens to expire. if err := s.DeleteUserSessions(cmd.Context(), args[0]); err != nil { return err } cmd.PrintErrln("Their sessions were closed. Their API keys still work — revoke those separately.") } fmt.Fprintf(cmd.OutOrStdout(), "%s is now %s\n", args[0], state) return nil }, } } func newUserAdminCmd(open userOpener, verb string, admin bool, short string) *cobra.Command { return &cobra.Command{ Use: verb + " ", Short: short, Long: "The dashboard is read-only, so this grants looking and nothing else.\n" + "Everything that changes the cluster or issues a credential stays here on\n" + "the CLI, where it needs a shell rather than a session — which keeps the\n" + "worst case of a stolen admin cookie to \"saw the cluster\".", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { s, err := open(cmd) if err != nil { return err } defer s.Close() if err := s.SetAdmin(cmd.Context(), args[0], admin); err != nil { return err } u, err := s.UserByID(cmd.Context(), args[0]) if err != nil { return err } if admin { fmt.Fprintf(cmd.OutOrStdout(), "%s can now see /admin\n", u.Email) } else { fmt.Fprintf(cmd.OutOrStdout(), "%s can no longer see /admin\n", u.Email) cmd.PrintErrln("Their session stays open; the page is read-only. " + "Suspend the account to close it.") } return nil }, } } func newUserResetPasswordCmd(open userOpener) *cobra.Command { var password string cmd := &cobra.Command{ Use: "reset-password ", Short: "Set a new password for someone who lost theirs", Long: "There is no self-service reset, because this gateway sends no email and\n" + "a reset link with nowhere to send it is not a reset. Set one here, tell\n" + "them out of band, and have them change it from their profile.\n\n" + "Every session they have open is closed: if they lost the password, the\n" + "reason might be that somebody else has it.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if password == "" { return fmt.Errorf("--password is required") } s, err := open(cmd) if err != nil { return err } defer s.Close() hash, err := account.HashPassword(password) if err != nil { return err } if err := s.SetPassword(cmd.Context(), args[0], hash); err != nil { return err } if err := s.DeleteUserSessions(cmd.Context(), args[0]); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "password set for %s\n", args[0]) cmd.PrintErrln("Their sessions were closed. Their API keys still work.") return nil }, } cmd.Flags().StringVar(&password, "password", "", "the new password") return cmd } func newUserCreateCmd(open userOpener) *cobra.Command { var project, password string cmd := &cobra.Command{ Use: "create ", Short: "Create an account directly, already approved", Long: "For when signup is closed, or for the first account on a new deployment.\n" + "The password is set here and should be changed by its owner.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if password == "" { return fmt.Errorf("--password is required") } s, err := open(cmd) if err != nil { return err } defer s.Close() hash, err := account.HashPassword(password) if err != nil { return err } id := randomID("usr") u := store.User{ ID: id, Email: args[0], Name: args[0], PasswordHash: hash, State: store.UserPending, CreatedAt: time.Now(), } if err := s.CreateUser(cmd.Context(), u); err != nil { return err } if err := s.ApproveUser(cmd.Context(), id, project, "cli", time.Now()); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s\n", id) cmd.PrintErrf("%s created and approved into project %s\n", args[0], project) return nil }, } cmd.Flags().StringVar(&project, "project", "default", "project the person's keys belong to") cmd.Flags().StringVar(&password, "password", "", "initial password") return cmd }