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/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/action"
|
"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/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gotosocial"
|
"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",
|
Name: "db",
|
||||||
Usage: "database-related tasks and utils",
|
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)
|
return fmt.Errorf("error creating config: %s", err)
|
||||||
}
|
}
|
||||||
// ... and the flags set on the *cli.Context by urfave
|
// ... 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
|
// create a logger with the log level, formatting, and output splitter already set
|
||||||
log, err := log.New(conf.LogLevel)
|
log, err := log.New(conf.LogLevel)
|
||||||
|
209
internal/clitools/admin/account/account.go
Normal file
209
internal/clitools/admin/account/account.go
Normal file
@ -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
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"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
|
// Config pulls together all the configuration needed to run gotosocial
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
/*
|
||||||
|
Parseable from .yaml configuration file.
|
||||||
|
For long-running commands (server start etc).
|
||||||
|
*/
|
||||||
|
|
||||||
LogLevel string `yaml:"logLevel"`
|
LogLevel string `yaml:"logLevel"`
|
||||||
ApplicationName string `yaml:"applicationName"`
|
ApplicationName string `yaml:"applicationName"`
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
@ -38,6 +55,12 @@ type Config struct {
|
|||||||
StorageConfig *StorageConfig `yaml:"storage"`
|
StorageConfig *StorageConfig `yaml:"storage"`
|
||||||
StatusesConfig *StatusesConfig `yaml:"statuses"`
|
StatusesConfig *StatusesConfig `yaml:"statuses"`
|
||||||
LetsEncryptConfig *LetsEncryptConfig `yaml:"letsEncrypt"`
|
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.
|
// FromFile returns a new config from a file, or an error if something goes amiss.
|
||||||
@ -62,6 +85,7 @@ func Empty() *Config {
|
|||||||
StorageConfig: &StorageConfig{},
|
StorageConfig: &StorageConfig{},
|
||||||
StatusesConfig: &StatusesConfig{},
|
StatusesConfig: &StatusesConfig{},
|
||||||
LetsEncryptConfig: &LetsEncryptConfig{},
|
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
|
// 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()
|
fn := GetFlagNames()
|
||||||
|
|
||||||
// For all of these flags, we only want to set them on the config if:
|
// 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) {
|
if c.Host == "" || f.IsSet(fn.Host) {
|
||||||
c.Host = f.String(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) {
|
if c.Protocol == "" || f.IsSet(fn.Protocol) {
|
||||||
c.Protocol = f.String(fn.Protocol)
|
c.Protocol = f.String(fn.Protocol)
|
||||||
}
|
}
|
||||||
|
if c.Protocol == "" {
|
||||||
|
return errors.New("protocol was not set")
|
||||||
|
}
|
||||||
|
|
||||||
// db flags
|
// db flags
|
||||||
if c.DBConfig.Type == "" || f.IsSet(fn.DbType) {
|
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) {
|
if c.LetsEncryptConfig.EmailAddress == "" || f.IsSet(fn.LetsEncryptEmailAddress) {
|
||||||
c.LetsEncryptConfig.EmailAddress = f.String(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.
|
// 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)
|
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{
|
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||||
APActivityType: gtsmodel.ActivityStreamsAccept,
|
APActivityType: gtsmodel.ActivityStreamsAccept,
|
||||||
GTSModel: follow,
|
GTSModel: follow,
|
||||||
|
OriginAccount: originAccount,
|
||||||
|
TargetAccount: targetAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)
|
gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)
|
||||||
|
Loading…
Reference in New Issue
Block a user