Admin cli (#29)
Now you can use the CLI tool to: * Create a new account with the given username, email address and password (which will be hashed of course). * Confirm the account's so that it can log in and post. * Promote the account to admin. * Demote the account from admin. * Disable the account. * Suspend the account.
This commit is contained in:
parent
0df2e18cc0
commit
43c3a47773
|
@ -24,6 +24,7 @@ import (
|
|||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/action"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/clitools/admin/account"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gotosocial"
|
||||
|
@ -263,6 +264,104 @@ func main() {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "admin",
|
||||
Usage: "gotosocial admin-related tasks",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "account",
|
||||
Usage: "admin commands related to accounts",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "create",
|
||||
Usage: "create a new account",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: config.UsernameFlag,
|
||||
Usage: config.UsernameUsage,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: config.EmailFlag,
|
||||
Usage: config.EmailUsage,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: config.PasswordFlag,
|
||||
Usage: config.PasswordUsage,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return runAction(c, account.Create)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "confirm",
|
||||
Usage: "confirm an existing account manually, thereby skipping email confirmation",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: config.UsernameFlag,
|
||||
Usage: config.UsernameUsage,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return runAction(c, account.Confirm)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "promote",
|
||||
Usage: "promote an account to admin",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: config.UsernameFlag,
|
||||
Usage: config.UsernameUsage,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return runAction(c, account.Promote)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "demote",
|
||||
Usage: "demote an account from admin to normal user",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: config.UsernameFlag,
|
||||
Usage: config.UsernameUsage,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return runAction(c, account.Demote)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "disable",
|
||||
Usage: "prevent an account from signing in or posting etc, but don't delete anything",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: config.UsernameFlag,
|
||||
Usage: config.UsernameUsage,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return runAction(c, account.Disable)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "suspend",
|
||||
Usage: "completely remove an account and all of its posts, media, etc",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: config.UsernameFlag,
|
||||
Usage: config.UsernameUsage,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return runAction(c, account.Suspend)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "db",
|
||||
Usage: "database-related tasks and utils",
|
||||
|
@ -308,7 +407,9 @@ func runAction(c *cli.Context, a action.GTSAction) error {
|
|||
return fmt.Errorf("error creating config: %s", err)
|
||||
}
|
||||
// ... and the flags set on the *cli.Context by urfave
|
||||
conf.ParseCLIFlags(c)
|
||||
if err := conf.ParseCLIFlags(c); err != nil {
|
||||
return fmt.Errorf("error parsing config: %s", err)
|
||||
}
|
||||
|
||||
// create a logger with the log level, formatting, and output splitter already set
|
||||
log, err := log.New(conf.LogLevel)
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package account
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/action"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db/pg"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// Create creates a new account in the database using the provided flags.
|
||||
var Create action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
|
||||
dbConn, err := pg.NewPostgresService(ctx, c, log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating dbservice: %s", err)
|
||||
}
|
||||
|
||||
username, ok := c.AccountCLIFlags[config.UsernameFlag]
|
||||
if !ok {
|
||||
return errors.New("no username set")
|
||||
}
|
||||
if err := util.ValidateUsername(username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
email, ok := c.AccountCLIFlags[config.EmailFlag]
|
||||
if !ok {
|
||||
return errors.New("no email set")
|
||||
}
|
||||
if err := util.ValidateEmail(email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, ok := c.AccountCLIFlags[config.PasswordFlag]
|
||||
if !ok {
|
||||
return errors.New("no password set")
|
||||
}
|
||||
if err := util.ValidateNewPassword(password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = dbConn.NewSignup(username, "", false, email, password, nil, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dbConn.Stop(ctx)
|
||||
}
|
||||
|
||||
// Confirm sets a user to Approved, sets Email to the current UnconfirmedEmail value, and sets ConfirmedAt to now.
|
||||
var Confirm action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
|
||||
dbConn, err := pg.NewPostgresService(ctx, c, log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating dbservice: %s", err)
|
||||
}
|
||||
|
||||
username, ok := c.AccountCLIFlags[config.UsernameFlag]
|
||||
if !ok {
|
||||
return errors.New("no username set")
|
||||
}
|
||||
if err := util.ValidateUsername(username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := >smodel.User{}
|
||||
if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Approved = true
|
||||
u.Email = u.UnconfirmedEmail
|
||||
u.ConfirmedAt = time.Now()
|
||||
if err := dbConn.UpdateByID(u.ID, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dbConn.Stop(ctx)
|
||||
}
|
||||
|
||||
// Promote sets a user to admin.
|
||||
var Promote action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
|
||||
dbConn, err := pg.NewPostgresService(ctx, c, log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating dbservice: %s", err)
|
||||
}
|
||||
|
||||
username, ok := c.AccountCLIFlags[config.UsernameFlag]
|
||||
if !ok {
|
||||
return errors.New("no username set")
|
||||
}
|
||||
if err := util.ValidateUsername(username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := >smodel.User{}
|
||||
if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
|
||||
return err
|
||||
}
|
||||
u.Admin = true
|
||||
if err := dbConn.UpdateByID(u.ID, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dbConn.Stop(ctx)
|
||||
}
|
||||
|
||||
// Demote sets admin on a user to false.
|
||||
var Demote action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
|
||||
dbConn, err := pg.NewPostgresService(ctx, c, log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating dbservice: %s", err)
|
||||
}
|
||||
|
||||
username, ok := c.AccountCLIFlags[config.UsernameFlag]
|
||||
if !ok {
|
||||
return errors.New("no username set")
|
||||
}
|
||||
if err := util.ValidateUsername(username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := >smodel.User{}
|
||||
if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
|
||||
return err
|
||||
}
|
||||
u.Admin = false
|
||||
if err := dbConn.UpdateByID(u.ID, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dbConn.Stop(ctx)
|
||||
}
|
||||
|
||||
// Disable sets Disabled to true on a user.
|
||||
var Disable action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
|
||||
dbConn, err := pg.NewPostgresService(ctx, c, log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating dbservice: %s", err)
|
||||
}
|
||||
|
||||
username, ok := c.AccountCLIFlags[config.UsernameFlag]
|
||||
if !ok {
|
||||
return errors.New("no username set")
|
||||
}
|
||||
if err := util.ValidateUsername(username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := >smodel.User{}
|
||||
if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
|
||||
return err
|
||||
}
|
||||
u.Disabled = true
|
||||
if err := dbConn.UpdateByID(u.ID, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dbConn.Stop(ctx)
|
||||
}
|
||||
|
||||
var Suspend action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
|
@ -19,14 +19,31 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
UsernameFlag = "username"
|
||||
UsernameUsage = "the username to create/delete/etc"
|
||||
|
||||
EmailFlag = "email"
|
||||
EmailUsage = "the email address of this account"
|
||||
|
||||
PasswordFlag = "password"
|
||||
PasswordUsage = "the password to set for this account"
|
||||
)
|
||||
|
||||
// Config pulls together all the configuration needed to run gotosocial
|
||||
type Config struct {
|
||||
/*
|
||||
Parseable from .yaml configuration file.
|
||||
For long-running commands (server start etc).
|
||||
*/
|
||||
|
||||
LogLevel string `yaml:"logLevel"`
|
||||
ApplicationName string `yaml:"applicationName"`
|
||||
Host string `yaml:"host"`
|
||||
|
@ -38,6 +55,12 @@ type Config struct {
|
|||
StorageConfig *StorageConfig `yaml:"storage"`
|
||||
StatusesConfig *StatusesConfig `yaml:"statuses"`
|
||||
LetsEncryptConfig *LetsEncryptConfig `yaml:"letsEncrypt"`
|
||||
|
||||
/*
|
||||
Not parsed from .yaml configuration file.
|
||||
For short running commands (admin CLI tools etc).
|
||||
*/
|
||||
AccountCLIFlags map[string]string
|
||||
}
|
||||
|
||||
// FromFile returns a new config from a file, or an error if something goes amiss.
|
||||
|
@ -62,6 +85,7 @@ func Empty() *Config {
|
|||
StorageConfig: &StorageConfig{},
|
||||
StatusesConfig: &StatusesConfig{},
|
||||
LetsEncryptConfig: &LetsEncryptConfig{},
|
||||
AccountCLIFlags: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +105,7 @@ func loadFromFile(path string) (*Config, error) {
|
|||
}
|
||||
|
||||
// ParseCLIFlags sets flags on the config using the provided Flags object
|
||||
func (c *Config) ParseCLIFlags(f KeyedFlags) {
|
||||
func (c *Config) ParseCLIFlags(f KeyedFlags) error {
|
||||
fn := GetFlagNames()
|
||||
|
||||
// For all of these flags, we only want to set them on the config if:
|
||||
|
@ -104,10 +128,16 @@ func (c *Config) ParseCLIFlags(f KeyedFlags) {
|
|||
if c.Host == "" || f.IsSet(fn.Host) {
|
||||
c.Host = f.String(fn.Host)
|
||||
}
|
||||
if c.Host == "" {
|
||||
return errors.New("host was not set")
|
||||
}
|
||||
|
||||
if c.Protocol == "" || f.IsSet(fn.Protocol) {
|
||||
c.Protocol = f.String(fn.Protocol)
|
||||
}
|
||||
if c.Protocol == "" {
|
||||
return errors.New("protocol was not set")
|
||||
}
|
||||
|
||||
// db flags
|
||||
if c.DBConfig.Type == "" || f.IsSet(fn.DbType) {
|
||||
|
@ -215,6 +245,15 @@ func (c *Config) ParseCLIFlags(f KeyedFlags) {
|
|||
if c.LetsEncryptConfig.EmailAddress == "" || f.IsSet(fn.LetsEncryptEmailAddress) {
|
||||
c.LetsEncryptConfig.EmailAddress = f.String(fn.LetsEncryptEmailAddress)
|
||||
}
|
||||
|
||||
// command-specific flags
|
||||
|
||||
// admin account CLI flags
|
||||
c.AccountCLIFlags[UsernameFlag] = f.String(UsernameFlag)
|
||||
c.AccountCLIFlags[EmailFlag] = f.String(EmailFlag)
|
||||
c.AccountCLIFlags[PasswordFlag] = f.String(PasswordFlag)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyedFlags is a wrapper for any type that can store keyed flags and give them back.
|
||||
|
|
|
@ -54,9 +54,22 @@ func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*ap
|
|||
return nil, NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
originAccount := >smodel.Account{}
|
||||
if err := p.db.GetByID(follow.AccountID, originAccount); err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
targetAccount := >smodel.Account{}
|
||||
if err := p.db.GetByID(follow.TargetAccountID, targetAccount); err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||
APActivityType: gtsmodel.ActivityStreamsAccept,
|
||||
GTSModel: follow,
|
||||
OriginAccount: originAccount,
|
||||
TargetAccount: targetAccount,
|
||||
}
|
||||
|
||||
gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)
|
||||
|
|
Loading…
Reference in New Issue