github.com/decred/politeia@v1.4.0/politeiawww/cmd/politeiawww_dbutil/politeiawww_dbutil.go (about)

     1  // Copyright (c) 2017-2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"crypto/tls"
    11  	"crypto/x509"
    12  	"encoding/hex"
    13  	"encoding/json"
    14  	"encoding/pem"
    15  	"errors"
    16  	"flag"
    17  	"fmt"
    18  	"io"
    19  	"net/url"
    20  	"os"
    21  	"path/filepath"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/davecgh/go-spew/spew"
    27  	"github.com/decred/dcrd/chaincfg/v3"
    28  	"github.com/decred/politeia/politeiad/api/v1/identity"
    29  	"github.com/decred/politeia/politeiad/backend/gitbe"
    30  	"github.com/decred/politeia/politeiad/backend/gitbe/decredplugin"
    31  	"github.com/decred/politeia/politeiawww/config"
    32  	"github.com/decred/politeia/politeiawww/legacy/user"
    33  	"github.com/decred/politeia/politeiawww/legacy/user/cockroachdb"
    34  	"github.com/decred/politeia/politeiawww/legacy/user/localdb"
    35  	mysqldb "github.com/decred/politeia/politeiawww/legacy/user/mysql"
    36  	"github.com/decred/politeia/util"
    37  	"github.com/google/uuid"
    38  	_ "github.com/jinzhu/gorm/dialects/postgres"
    39  	"github.com/marcopeereboom/sbox"
    40  )
    41  
    42  const (
    43  	defaultMySQLHost       = "localhost:3306"
    44  	defaultCockroachDBHost = "localhost:26257"
    45  	// The following hardcoded CockroachDB paths are not ideal, instead they
    46  	// should use OS specific paths:
    47  	// `dcrutil.AppDataDir("cockroachdb", false)`, but since we use
    48  	// `~/.cockroachdb` in our script to generate the CockroachDB certs (see
    49  	// `/scripts/cockroachcerts.sh`) we are limited to use the same hardcoded
    50  	// paths here.
    51  	defaultRootCert   = "~/.cockroachdb/certs/clients/politeiawww/ca.crt"
    52  	defaultClientCert = "~/.cockroachdb/certs/clients/politeiawww/client.politeiawww.crt"
    53  	defaultClientKey  = "~/.cockroachdb/certs/clients/politeiawww/client.politeiawww.key"
    54  
    55  	// Politeia repo info
    56  	commentsJournalFilename = "comments.journal"
    57  
    58  	// Journal actions
    59  	journalActionAdd = "add" // Add entry
    60  	journalActionDel = "del" // Delete entry
    61  )
    62  
    63  var (
    64  	defaultHomeDir       = config.DefaultHomeDir
    65  	defaultDataDir       = config.DefaultDataDir
    66  	defaultEncryptionKey = filepath.Join(defaultHomeDir, "sbox.key")
    67  
    68  	// Database options
    69  	level     = flag.Bool("leveldb", false, "")
    70  	cockroach = flag.Bool("cockroachdb", false, "")
    71  	mysql     = flag.Bool("mysql", false, "")
    72  
    73  	// Application options
    74  	testnet         = flag.Bool("testnet", false, "")
    75  	dataDir         = flag.String("datadir", defaultDataDir, "")
    76  	cockroachdbhost = flag.String("cockroachdbhost", defaultCockroachDBHost, "")
    77  	mysqlhost       = flag.String("mysqlhost", defaultMySQLHost, "")
    78  
    79  	rootCert      = flag.String("rootcert", defaultRootCert, "")
    80  	clientCert    = flag.String("clientcert", defaultClientCert, "")
    81  	clientKey     = flag.String("clientkey", defaultClientKey, "")
    82  	encryptionKey = flag.String("encryptionkey", defaultEncryptionKey, "")
    83  	password      = flag.String("password", "", "")
    84  
    85  	// Commands
    86  	addCredits       = flag.Bool("addcredits", false, "")
    87  	dump             = flag.Bool("dump", false, "")
    88  	setAdmin         = flag.Bool("setadmin", false, "")
    89  	setEmail         = flag.Bool("setemail", false, "")
    90  	stubUsers        = flag.Bool("stubusers", false, "")
    91  	migrate          = flag.Bool("migrate", false, "")
    92  	createKey        = flag.Bool("createkey", false, "")
    93  	verifyIdentities = flag.Bool("verifyidentities", false, "")
    94  	resetTotp        = flag.Bool("resettotp", false, "")
    95  
    96  	network string // Mainnet or testnet3
    97  	userDB  user.Database
    98  )
    99  
   100  const usageMsg = `politeiawww_dbutil usage:
   101    Database options
   102      -leveldb
   103            Use LevelDB
   104      -cockroachdb
   105            Use CockroachDB
   106      -mysql
   107            Use MySQL
   108  
   109    Application options
   110      -testnet
   111            Use testnet database
   112      -datadir string
   113            politeiawww data directory
   114            (default osDataDir/politeiawww/data)
   115      -cockroachdbhost string
   116            CockroachDB ip:port 
   117            (default localhost:26257)
   118      -rootcert string
   119            File containing the CockroachDB SSL root cert
   120            (default ~/.cockroachdb/certs/clients/politeiawww/ca.crt)
   121      -clientcert string
   122            File containing the CockroachDB SSL client cert
   123            (default ~/.cockroachdb/certs/clients/politeiawww/client.politeiawww.crt)
   124      -clientkey string
   125            File containing the CockroachDB SSL client cert key
   126            (default ~/.cockroachdb/certs/clients/politeiawww/client.politeiawww.key)
   127      -encryptionkey string
   128            File containing the CockroachDB/MySQL encryption key
   129            (default osDataDir/politeiawww/sbox.key)
   130      -password string
   131            MySQL database password.
   132      -mysqlhost string
   133            MySQL ip:port 
   134            (default localhost:3306)
   135  
   136    Commands
   137      -addcredits
   138            Add proposal credits to a user's account
   139            Required DB flag : -leveldb, -cockroachdb or -mysql
   140            LevelDB args     : <email> <quantity>
   141            CockroachDB args : <username> <quantity>
   142      -setadmin
   143            Set the admin flag for a user
   144            Required DB flag : -leveldb, -cockroachdb or -mysql
   145            LevelDB args     : <email> <true/false>
   146            CockroachDB args : <username> <true/false>
   147      -setemail
   148            Set a user's email to the provided email address
   149            Required DB flag : -cockroachdb or -mysql
   150            CockroachDB args : <username> <email>
   151      -stubusers
   152            Create user stubs for the public keys in a politeia repo
   153            Required DB flag : -leveldb, -cockroachdb or -mysql
   154            LevelDB args     : <importDir>
   155            CockroachDB args : <importDir>
   156      -dump
   157            Dump the entire database or the contents of a specific user
   158            Required DB flag : -leveldb or -cockroachdb or -mysql
   159            LevelDB args     : <username>
   160      -createkey
   161            Create a new encryption key that can be used to encrypt data at rest
   162            Required DB flag : None
   163            Args             : <destination (optional)>
   164                               (default osDataDir/politeiawww/sbox.key)
   165      -migrate
   166            Migrate from one user database to another
   167            Required DB flag : None
   168            Args             : <fromDB> <toDB>
   169                               Valid DBs are mysql, cockroachdb, leveldb
   170      -verifyidentities
   171            Verify a user's identities do not violate any politeia rules. Invalid
   172            identities are fixed.
   173            Required DB flag : -cockroachdb or -mysql
   174      -resettotp
   175            Reset a user's totp settings in case they are locked out and 
   176            confirm identity. 
   177            Required DB flag : -leveldb, -cockroachdb or -mysql
   178            LevelDB args     : <email>
   179            CockroachDB args : <username>`
   180  
   181  func cmdDump() error {
   182  	args := flag.Args()
   183  	if len(args) == 0 {
   184  		return fmt.Errorf("username was not provided")
   185  	}
   186  
   187  	username := args[0]
   188  	u, err := userDB.UserGetByUsername(username)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	fmt.Printf("%v", spew.Sdump(u))
   194  
   195  	return nil
   196  }
   197  
   198  func cmdSetAdmin() error {
   199  	args := flag.Args()
   200  	if len(args) < 2 {
   201  		flag.Usage()
   202  		return nil
   203  	}
   204  
   205  	username := args[0]
   206  	isAdmin := (strings.ToLower(args[1]) == "true" || args[1] == "1")
   207  
   208  	u, err := userDB.UserGetByUsername(username)
   209  	if err != nil {
   210  		return err
   211  	}
   212  
   213  	u.Admin = isAdmin
   214  
   215  	err = userDB.UserUpdate(*u)
   216  	if err != nil {
   217  		return err
   218  	}
   219  
   220  	fmt.Printf("User with username '%v' admin status updated "+
   221  		"to %v\n", username, isAdmin)
   222  
   223  	return nil
   224  }
   225  
   226  func cmdSetEmail() error {
   227  	args := flag.Args()
   228  	if len(args) < 2 {
   229  		flag.Usage()
   230  		return nil
   231  	}
   232  
   233  	if *level {
   234  		return fmt.Errorf("this cannot be used with the -leveldb flag")
   235  	}
   236  
   237  	username := strings.ToLower(args[0])
   238  	newEmail := strings.ToLower(args[1])
   239  
   240  	u, err := userDB.UserGetByUsername(username)
   241  	if err != nil {
   242  		return err
   243  	}
   244  
   245  	u.Email = newEmail
   246  
   247  	err = userDB.UserUpdate(*u)
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	fmt.Printf("User with username '%v' email successfully updated to '%v'\n",
   253  		username, newEmail)
   254  	fmt.Printf("politeiawww MUST BE restarted so the user email memory cache " +
   255  		"gets updated; politeiad is fine and does not need to be restarted\n")
   256  
   257  	return nil
   258  }
   259  
   260  func cmdAddCredits() error {
   261  	args := flag.Args()
   262  	if len(args) < 2 {
   263  		flag.Usage()
   264  		return nil
   265  	}
   266  	username := args[0]
   267  
   268  	quantity, err := strconv.Atoi(args[1])
   269  	if err != nil {
   270  		return fmt.Errorf("parse int '%v' failed: %v",
   271  			args[1], err)
   272  	}
   273  	// Lookup user
   274  	u, err := userDB.UserGetByUsername(username)
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	// Create proposal credits
   280  	ts := time.Now().Unix()
   281  	c := make([]user.ProposalCredit, 0, quantity)
   282  	for i := 0; i < quantity; i++ {
   283  		c = append(c, user.ProposalCredit{
   284  			PaywallID:     0,
   285  			Price:         0,
   286  			DatePurchased: ts,
   287  			TxID:          "created_by_dbutil",
   288  		})
   289  	}
   290  	u.UnspentProposalCredits = append(u.UnspentProposalCredits, c...)
   291  
   292  	// Update database
   293  	err = userDB.UserUpdate(*u)
   294  	if err != nil {
   295  		return fmt.Errorf("update user: %v", err)
   296  	}
   297  
   298  	fmt.Printf("%v proposal credits added to account %v\n",
   299  		quantity, username)
   300  
   301  	return nil
   302  }
   303  
   304  func replayCommentsJournal(path string, pubkeys map[string]struct{}) error {
   305  	b, err := os.ReadFile(path)
   306  	if err != nil {
   307  		return err
   308  	}
   309  	d := json.NewDecoder(bytes.NewReader(b))
   310  
   311  	for {
   312  		var action gitbe.JournalAction
   313  		err = d.Decode(&action)
   314  		if errors.Is(err, io.EOF) {
   315  			break
   316  		} else if err != nil {
   317  			return fmt.Errorf("journal action: %v", err)
   318  		}
   319  
   320  		switch action.Action {
   321  		case journalActionAdd:
   322  			var c decredplugin.Comment
   323  			err = d.Decode(&c)
   324  			if err != nil {
   325  				return fmt.Errorf("journal add: %v", err)
   326  			}
   327  			pubkeys[c.PublicKey] = struct{}{}
   328  
   329  		case journalActionDel:
   330  			var cc decredplugin.CensorComment
   331  			err = d.Decode(&cc)
   332  			if err != nil {
   333  				return fmt.Errorf("journal censor: %v", err)
   334  			}
   335  			pubkeys[cc.PublicKey] = struct{}{}
   336  
   337  		default:
   338  			return fmt.Errorf("invalid action: %v",
   339  				action.Action)
   340  		}
   341  	}
   342  
   343  	return nil
   344  }
   345  
   346  func cmdStubUsers() error {
   347  	if len(flag.Args()) == 0 {
   348  		return fmt.Errorf("must provide import directory")
   349  	}
   350  
   351  	// Parse import directory
   352  	importDir := util.CleanAndExpandPath(flag.Arg(0))
   353  	_, err := os.Stat(importDir)
   354  	if err != nil {
   355  		return err
   356  	}
   357  
   358  	// Walk import directory and compile all unique public
   359  	// keys that are found.
   360  	fmt.Printf("Walking import directory...\n")
   361  	pubkeys := make(map[string]struct{})
   362  	err = filepath.Walk(importDir,
   363  		func(path string, info os.FileInfo, err error) error {
   364  			if err != nil {
   365  				return err
   366  			}
   367  
   368  			// Skip directories
   369  			if info.IsDir() {
   370  				return nil
   371  			}
   372  
   373  			switch info.Name() {
   374  			case commentsJournalFilename:
   375  				err := replayCommentsJournal(path, pubkeys)
   376  				if err != nil {
   377  					return err
   378  				}
   379  			}
   380  
   381  			return nil
   382  		})
   383  	if err != nil {
   384  		return fmt.Errorf("walk import dir: %v", err)
   385  	}
   386  
   387  	fmt.Printf("Stubbing users...\n")
   388  
   389  	// update users on database
   390  	var i int
   391  	for k := range pubkeys {
   392  		username := fmt.Sprintf("dbutil_user%v", i)
   393  		email := username + "@example.com"
   394  		id, err := identity.PublicIdentityFromString(k)
   395  		if err != nil {
   396  			return err
   397  		}
   398  
   399  		err = userDB.UserNew(user.User{
   400  			ID:             uuid.New(),
   401  			Email:          email,
   402  			Username:       username,
   403  			HashedPassword: []byte("password"),
   404  			Admin:          false,
   405  			Identities: []user.Identity{
   406  				{
   407  					Key:       id.Key,
   408  					Activated: time.Now().Unix(),
   409  				},
   410  			},
   411  		})
   412  		if err != nil {
   413  			return err
   414  		}
   415  
   416  		i++
   417  	}
   418  
   419  	fmt.Printf("Done!\n")
   420  	return nil
   421  }
   422  
   423  func connectLevelDB() (user.Database, error) {
   424  	dbDir := filepath.Join(*dataDir, network)
   425  	_, err := os.Stat(dbDir)
   426  	if err != nil {
   427  		if os.IsNotExist(err) {
   428  			err = fmt.Errorf("leveldb dir not found: %v", dbDir)
   429  		}
   430  		return nil, err
   431  	}
   432  
   433  	fmt.Printf("LevelDB     : %v\n", dbDir)
   434  	return localdb.New(dbDir)
   435  }
   436  
   437  func connectCockroachDB() (user.Database, error) {
   438  	err := validateCockroachParams()
   439  	if err != nil {
   440  		return nil, fmt.Errorf("new cockroachdb: %v", err)
   441  	}
   442  
   443  	fmt.Printf("CockroachDB : %v %v", *cockroachdbhost, network)
   444  
   445  	return cockroachdb.New(*cockroachdbhost, network, *rootCert,
   446  		*clientCert, *clientKey, *encryptionKey)
   447  }
   448  
   449  func connectMySQL() (user.Database, error) {
   450  	err := validateMySQLParams()
   451  	if err != nil {
   452  		return nil, err
   453  	}
   454  
   455  	fmt.Printf("MySQL : %v %v\n", *mysqlhost, network)
   456  
   457  	return mysqldb.New(*mysqlhost, *password, network, *encryptionKey)
   458  }
   459  
   460  func connectDB(typeDB string) (user.Database, error) {
   461  	switch typeDB {
   462  	case "leveldb":
   463  		return connectLevelDB()
   464  
   465  	case "cockroachdb":
   466  		return connectCockroachDB()
   467  
   468  	case "mysql":
   469  		return connectMySQL()
   470  
   471  	default:
   472  		return nil, fmt.Errorf("invalid database type: %v", typeDB)
   473  	}
   474  }
   475  
   476  func cmdMigrate() error {
   477  	args := flag.Args()
   478  	if len(args) < 2 {
   479  		flag.Usage()
   480  		return nil
   481  	}
   482  
   483  	var (
   484  		fromType = args[0]
   485  		toType   = args[1]
   486  	)
   487  	if fromType == toType {
   488  		return fmt.Errorf("origin and destination databases cannot be the same")
   489  	}
   490  
   491  	// Connect to the databases
   492  	fromDB, err := connectDB(fromType)
   493  	if err != nil {
   494  		return err
   495  	}
   496  	defer fromDB.Close()
   497  
   498  	toDB, err := connectDB(toType)
   499  	if err != nil {
   500  		return err
   501  	}
   502  	defer toDB.Close()
   503  
   504  	fmt.Printf("Migrating users from %v to %v...\n", fromType, toType)
   505  
   506  	// Migrate the users
   507  	var (
   508  		paywallIndex uint64
   509  		userCount    int
   510  	)
   511  	err = fromDB.AllUsers(func(u *user.User) {
   512  		// Record the highest paywall address index found in the
   513  		// database. This will be saved in the new database once
   514  		// all of the users have been migrated.
   515  		if u.PaywallAddressIndex > paywallIndex {
   516  			paywallIndex = u.PaywallAddressIndex
   517  		}
   518  
   519  		// Check if username already exists. There was a ~2
   520  		// month period where a bug allowed for users to be
   521  		// created with duplicate usernames.
   522  		_, err = toDB.UserGetByUsername(u.Username)
   523  		switch err {
   524  		case user.ErrUserNotFound:
   525  			// Username doesn't exist; continue
   526  
   527  		case nil:
   528  			// The username already exists in the database. Allow the
   529  			// caller to update the username so that it's unique.
   530  			for !errors.Is(err, user.ErrUserNotFound) {
   531  				fmt.Printf("Username '%v' already exists. Username must be "+
   532  					"updated for the following user before the migration can "+
   533  					"continue.\n", u.Username)
   534  
   535  				fmt.Printf("ID                 : %v\n", u.ID.String())
   536  				fmt.Printf("Email              : %v\n", u.Email)
   537  				fmt.Printf("Username           : %v\n", u.Username)
   538  				fmt.Printf("Input new username : ")
   539  
   540  				var input string
   541  				r := bufio.NewReader(os.Stdin)
   542  				input, err = r.ReadString('\n')
   543  				if err != nil {
   544  					panic(err)
   545  				}
   546  				username := strings.TrimSuffix(input, "\n")
   547  				username = strings.ToLower(strings.TrimSpace(username))
   548  
   549  				u.Username = username
   550  
   551  				// Verify that the updated username is unique
   552  				_, err = toDB.UserGetByUsername(u.Username)
   553  			}
   554  
   555  			fmt.Printf("Username updated to '%v'\n", u.Username)
   556  
   557  		default:
   558  			panic(err)
   559  		}
   560  
   561  		err = toDB.InsertUser(*u)
   562  		if err != nil {
   563  			panic(fmt.Sprintf("InsertUser %v: %v", u.ID, err))
   564  		}
   565  		userCount++
   566  	})
   567  	if err != nil {
   568  		return fmt.Errorf("AllUsers: %v", err)
   569  	}
   570  	if userCount == 0 {
   571  		fmt.Printf("No users found\n")
   572  		return nil
   573  	}
   574  
   575  	// Save the paywall address index to the new database.
   576  	// The index should be the same value as the number of
   577  	// users in the database. If it's not, update it and
   578  	// inform the caller. This can happen if the database
   579  	// has user stubs in it.
   580  	if int(paywallIndex) < userCount {
   581  		fmt.Printf("WARN: Paywall address index does not match the "+
   582  			"user count; user count %v, paywall address index %v\n",
   583  			userCount, paywallIndex)
   584  
   585  		paywallIndex = uint64(userCount)
   586  
   587  		fmt.Printf("Updated paywall address index to %v\n", paywallIndex)
   588  	}
   589  
   590  	err = toDB.SetPaywallAddressIndex(paywallIndex)
   591  	if err != nil {
   592  		return fmt.Errorf("set paywall index '%v': %v", paywallIndex, err)
   593  	}
   594  
   595  	fmt.Printf("Users migrated : %v\n", userCount)
   596  	fmt.Printf("Paywall index  : %v\n", paywallIndex)
   597  	fmt.Printf("Done!\n")
   598  
   599  	return nil
   600  }
   601  
   602  func cmdCreateKey() error {
   603  	path := defaultEncryptionKey
   604  	args := flag.Args()
   605  	if len(args) > 0 {
   606  		path = util.CleanAndExpandPath(args[0])
   607  	}
   608  
   609  	// Don't allow overwriting an existing key
   610  	_, err := os.Stat(path)
   611  	if err == nil {
   612  		return fmt.Errorf("file already exists; cannot "+
   613  			"overwrite %v", path)
   614  	}
   615  
   616  	// Create a new key
   617  	k, err := sbox.NewKey()
   618  	if err != nil {
   619  		return err
   620  	}
   621  
   622  	// Write hex encoded key to file
   623  	err = os.WriteFile(path, []byte(hex.EncodeToString(k[:])), 0644)
   624  	if err != nil {
   625  		return err
   626  	}
   627  
   628  	fmt.Printf("Encryption key saved to: %v\n", path)
   629  
   630  	// Zero out encryption key
   631  	util.Zero(k[:])
   632  	k = nil
   633  
   634  	return nil
   635  }
   636  
   637  func validateCockroachParams() error {
   638  	// Validate host
   639  	_, err := url.Parse(*cockroachdbhost)
   640  	if err != nil {
   641  		return fmt.Errorf("parse host '%v': %v",
   642  			*cockroachdbhost, err)
   643  	}
   644  
   645  	// Validate root cert
   646  	b, err := os.ReadFile(*rootCert)
   647  	if err != nil {
   648  		return fmt.Errorf("read rootcert: %v", err)
   649  	}
   650  
   651  	block, _ := pem.Decode(b)
   652  	_, err = x509.ParseCertificate(block.Bytes)
   653  	if err != nil {
   654  		return fmt.Errorf("parse rootcert: %v", err)
   655  	}
   656  
   657  	// Validate client key pair
   658  	_, err = tls.LoadX509KeyPair(*clientCert, *clientKey)
   659  	if err != nil {
   660  		return fmt.Errorf("load key pair clientcert and "+
   661  			"clientkey: %v", err)
   662  	}
   663  
   664  	// Ensure encryption key file exists
   665  	if !util.FileExists(*encryptionKey) {
   666  		return fmt.Errorf("file not found %v", *encryptionKey)
   667  	}
   668  
   669  	return nil
   670  }
   671  
   672  func validateMySQLParams() error {
   673  	// Validate host.
   674  	_, err := url.Parse(*mysqlhost)
   675  	if err != nil {
   676  		return fmt.Errorf("parse host '%v': %v", *mysqlhost, err)
   677  	}
   678  
   679  	// Validate password.
   680  	if *password == "" {
   681  		return fmt.Errorf("MySQL politeiawww user's password is missing;" +
   682  			" use -password flag to provide it")
   683  	}
   684  
   685  	// Ensure encryption key file exists.
   686  	if !util.FileExists(*encryptionKey) {
   687  		return fmt.Errorf("file not found %v", *encryptionKey)
   688  	}
   689  
   690  	return nil
   691  }
   692  
   693  func cmdVerifyIdentities() error {
   694  	args := flag.Args()
   695  	if len(args) != 1 {
   696  		return fmt.Errorf("invalid number of arguments; want <username>, got %v",
   697  			args)
   698  	}
   699  
   700  	u, err := userDB.UserGetByUsername(args[0])
   701  	if err != nil {
   702  		return fmt.Errorf("UserGetByUsername(%v): %v",
   703  			args[0], err)
   704  	}
   705  
   706  	// Print all identities to help with debugging
   707  	fmt.Printf("\n")
   708  	for _, v := range u.Identities {
   709  		fmt.Printf("Status     : %v\n", v.Status())
   710  		fmt.Printf("Public Key : %v\n", v.String())
   711  		fmt.Printf("Activated  : %v\n", formatUnix(v.Activated))
   712  		fmt.Printf("Deactivated: %v\n", formatUnix(v.Deactivated))
   713  		fmt.Printf("\n")
   714  	}
   715  
   716  	// Verify inactive identities. There should only ever be one
   717  	// inactive identity at a time. If more than one inactive identity
   718  	// is found, deactivate all of them since it can't be determined
   719  	// which one is the most recent.
   720  	inactive := make(map[string]user.Identity, len(u.Identities)) // [pubkey]Identity
   721  	for _, v := range u.Identities {
   722  		if v.IsInactive() {
   723  			inactive[v.String()] = v
   724  		}
   725  	}
   726  	switch len(inactive) {
   727  	case 0:
   728  		fmt.Printf("0 inactive identities found; this is ok\n")
   729  	case 1:
   730  		fmt.Printf("1 inactive identity found; this is ok\n")
   731  	default:
   732  		fmt.Printf("%v inactive identities found\n", len(inactive))
   733  		for _, v := range inactive {
   734  			fmt.Printf("%v\n", v.String())
   735  		}
   736  
   737  		fmt.Printf("deactivating all inactive identities\n")
   738  
   739  		for i, v := range u.Identities {
   740  			if !v.IsInactive() {
   741  				// Not an inactive identity
   742  				continue
   743  			}
   744  			fmt.Printf("deactivating: %v\n", v.String())
   745  			u.Identities[i].Deactivate()
   746  		}
   747  	}
   748  
   749  	// Verify active identities. There should only ever be one active
   750  	// identity at a time.
   751  	active := make(map[string]user.Identity, len(u.Identities)) // [pubkey]Identity
   752  	for _, v := range u.Identities {
   753  		if v.IsActive() {
   754  			active[v.String()] = v
   755  		}
   756  	}
   757  	switch len(active) {
   758  	case 0:
   759  		fmt.Printf("0 active identities found; this is ok\n")
   760  	case 1:
   761  		fmt.Printf("1 active identity found; this is ok\n")
   762  	default:
   763  		fmt.Printf("%v active identities found\n", len(active))
   764  		for _, v := range active {
   765  			fmt.Printf("%v\n", v.String())
   766  		}
   767  
   768  		fmt.Printf("deactivating all but the most recent active identity\n")
   769  
   770  		// Find most recent active identity
   771  		var pubkey string
   772  		var ts int64
   773  		for _, v := range active {
   774  			if v.Activated > ts {
   775  				pubkey = v.String()
   776  				ts = v.Activated
   777  			}
   778  		}
   779  
   780  		// Deactivate all but the most recent active identity
   781  		for i, v := range u.Identities {
   782  			if !v.IsActive() {
   783  				// Not an active identity
   784  				continue
   785  			}
   786  			if pubkey == v.String() {
   787  				// Most recent active identity
   788  				continue
   789  			}
   790  			fmt.Printf("deactivating: %v\n", v.String())
   791  			u.Identities[i].Deactivate()
   792  		}
   793  	}
   794  
   795  	// Update user
   796  	err = userDB.UserUpdate(*u)
   797  	if err != nil {
   798  		return fmt.Errorf("UserUpdate: %v", err)
   799  	}
   800  
   801  	return nil
   802  }
   803  
   804  func cmdResetTOTP() error {
   805  	args := flag.Args()
   806  	if len(args) != 1 {
   807  		return fmt.Errorf("invalid number of arguments; want <username>, got %v",
   808  			args)
   809  	}
   810  
   811  	username := args[0]
   812  	u, err := userDB.UserGetByUsername(username)
   813  	if err != nil {
   814  		return err
   815  	}
   816  
   817  	u.TOTPLastUpdated = nil
   818  	u.TOTPSecret = ""
   819  	u.TOTPType = 0
   820  	u.TOTPVerified = false
   821  
   822  	err = userDB.UserUpdate(*u)
   823  	if err != nil {
   824  		return err
   825  	}
   826  
   827  	fmt.Printf("User with username '%v' reset totp\n", username)
   828  
   829  	return nil
   830  }
   831  
   832  func _main() error {
   833  	flag.Parse()
   834  
   835  	*dataDir = util.CleanAndExpandPath(*dataDir)
   836  	*rootCert = util.CleanAndExpandPath(*rootCert)
   837  	*clientCert = util.CleanAndExpandPath(*clientCert)
   838  	*clientKey = util.CleanAndExpandPath(*clientKey)
   839  	*encryptionKey = util.CleanAndExpandPath(*encryptionKey)
   840  
   841  	if *testnet {
   842  		network = chaincfg.TestNet3Params().Name
   843  	} else {
   844  		network = chaincfg.MainNetParams().Name
   845  	}
   846  
   847  	// Validate database selection.
   848  	switch {
   849  	case *mysql && *cockroach, *level && *mysql, *level && *cockroach,
   850  		*level && *cockroach && *mysql:
   851  		fmt.Println(mysql, cockroach)
   852  		return fmt.Errorf("multiple database flags; must use one of the " +
   853  			"following: -leveldb, -mysql or -cockroachdb")
   854  	}
   855  
   856  	switch {
   857  	case *addCredits || *setAdmin || *stubUsers || *resetTotp || *dump:
   858  		// These commands must be run with -cockroachdb, -mysql or -leveldb.
   859  		if !*level && !*cockroach && !*mysql {
   860  			return fmt.Errorf("missing database flag; must use " +
   861  				"-leveldb, -cockroachdb or -mysql")
   862  		}
   863  	case *verifyIdentities, *setEmail:
   864  		// These commands must be run with either -cockroachdb or -mysql.
   865  		if !*cockroach && !*mysql {
   866  			return fmt.Errorf("invalid database flag; must use " +
   867  				"either -mysql or -cockroachdb with this command")
   868  		}
   869  	case *migrate || *createKey:
   870  		// These commands must be run without a database flag.
   871  		if *level || *cockroach || *mysql {
   872  			return fmt.Errorf("unexpected database flag found; " +
   873  				"remove database flag -leveldb, -mysql and -cockroachdb")
   874  		}
   875  	}
   876  
   877  	// Connect to database
   878  	var err error
   879  	switch {
   880  	case *level:
   881  		userDB, err = connectLevelDB()
   882  
   883  	case *cockroach:
   884  		userDB, err = connectCockroachDB()
   885  
   886  	case *mysql:
   887  		userDB, err = connectMySQL()
   888  
   889  	}
   890  	if err != nil {
   891  		return err
   892  	}
   893  	if userDB != nil {
   894  		defer userDB.Close()
   895  	}
   896  
   897  	// Run command
   898  	switch {
   899  	case *addCredits:
   900  		return cmdAddCredits()
   901  	case *dump:
   902  		return cmdDump()
   903  	case *setAdmin:
   904  		return cmdSetAdmin()
   905  	case *setEmail:
   906  		return cmdSetEmail()
   907  	case *stubUsers:
   908  		return cmdStubUsers()
   909  	case *migrate:
   910  		return cmdMigrate()
   911  	case *createKey:
   912  		return cmdCreateKey()
   913  	case *verifyIdentities:
   914  		return cmdVerifyIdentities()
   915  	case *resetTotp:
   916  		return cmdResetTOTP()
   917  	default:
   918  		fmt.Printf("invalid command\n")
   919  		flag.Usage()
   920  	}
   921  
   922  	return nil
   923  }
   924  
   925  func main() {
   926  	// Custom usage message
   927  	flag.Usage = func() {
   928  		fmt.Fprintln(os.Stderr, usageMsg)
   929  	}
   930  
   931  	err := _main()
   932  	if err != nil {
   933  		fmt.Fprintf(os.Stderr, "%v\n", err)
   934  		os.Exit(1)
   935  	}
   936  }