source: code/trunk/cmd/sojuctl/main.go@ 324

Last change on this file since 324 was 324, checked in by contact, 5 years ago

Introduce User.Created

For Network and Channel, the database only needed to define one Store
operation to create/update a record. However since User is missing an ID
we couldn't have a single StoreUser function like other types. We had
CreateUser and UpdatePassword. As new User fields get added (e.g. the
upcoming Admin flag) this isn't sustainable.

We could have CreateUser and UpdateUser, but this wouldn't be consistent
with other types. Instead, introduce User.Created which indicates
whether the record is already stored in the DB. This can be used in a
new StoreUser function to decide whether we need to UPDATE or INSERT
without relying on SQL constraints and INSERT OR UPDATE.

The ListUsers and GetUser functions set User.Created to true.

File size: 2.6 KB
RevLine 
[98]1package main
2
3import (
[308]4 "bufio"
[98]5 "flag"
6 "fmt"
7 "log"
8 "os"
9
10 "git.sr.ht/~emersion/soju"
11 "git.sr.ht/~emersion/soju/config"
12 "golang.org/x/crypto/bcrypt"
13 "golang.org/x/crypto/ssh/terminal"
14)
15
16const usage = `usage: sojuctl [-config path] <action> [options...]
17
[251]18 create-user <username> Create a new user
19 change-password <username> Change password for a user
20 help Show this help message
[98]21`
22
23func init() {
24 flag.Usage = func() {
25 fmt.Fprintf(flag.CommandLine.Output(), usage)
26 }
27}
28
29func main() {
30 var configPath string
31 flag.StringVar(&configPath, "config", "", "path to configuration file")
32 flag.Parse()
33
34 var cfg *config.Server
35 if configPath != "" {
36 var err error
37 cfg, err = config.Load(configPath)
38 if err != nil {
39 log.Fatalf("failed to load config file: %v", err)
40 }
41 } else {
42 cfg = config.Defaults()
43 }
44
45 db, err := soju.OpenSQLDB(cfg.SQLDriver, cfg.SQLSource)
46 if err != nil {
47 log.Fatalf("failed to open database: %v", err)
48 }
49
50 switch cmd := flag.Arg(0); cmd {
51 case "create-user":
52 username := flag.Arg(1)
53 if username == "" {
54 flag.Usage()
55 os.Exit(1)
56 }
57
[308]58 password, err := readPassword()
[98]59 if err != nil {
60 log.Fatalf("failed to read password: %v", err)
61 }
62
63 hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
64 if err != nil {
65 log.Fatalf("failed to hash password: %v", err)
66 }
67
68 user := soju.User{
69 Username: username,
70 Password: string(hashed),
71 }
[324]72 if err := db.StoreUser(&user); err != nil {
[98]73 log.Fatalf("failed to create user: %v", err)
74 }
[251]75 case "change-password":
76 username := flag.Arg(1)
77 if username == "" {
78 flag.Usage()
79 os.Exit(1)
80 }
81
[308]82 password, err := readPassword()
[251]83 if err != nil {
[308]84 log.Fatalf("failed to read password: %v", err)
[251]85 }
86
87 hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
88 if err != nil {
89 log.Fatalf("failed to hash password: %v", err)
90 }
91
92 user := soju.User{
[324]93 Created: true,
[251]94 Username: username,
95 Password: string(hashed),
96 }
[324]97 if err := db.StoreUser(&user); err != nil {
[251]98 log.Fatalf("failed to update password: %v", err)
99 }
[98]100 default:
101 flag.Usage()
102 if cmd != "help" {
103 os.Exit(1)
104 }
105 }
106}
[308]107
108func readPassword() ([]byte, error) {
109 var password []byte
110 var err error
111 fd := int(os.Stdin.Fd())
112
113 if terminal.IsTerminal(fd) {
114 fmt.Printf("Password: ")
115 password, err = terminal.ReadPassword(int(os.Stdin.Fd()))
116 if err != nil {
117 return nil, err
118 }
119 fmt.Printf("\n")
120 } else {
121 fmt.Fprintf(os.Stderr, "Warning: Reading password from stdin.\n")
122 scanner := bufio.NewScanner(os.Stdin)
123 scanner.Scan()
124 password = scanner.Bytes()
125
126 if len(password) == 0 {
127 return nil, fmt.Errorf("zero length password")
128 }
129 }
130
131 return password, nil
132}
Note: See TracBrowser for help on using the repository browser.