diff --git a/NOTES.md b/NOTES.md index 7651e9622b809eaec101d56d024ad6e6caa856e1..4d60ccea91e43cd3b6edec9eab79554773ef8fc0 100644 --- a/NOTES.md +++ b/NOTES.md @@ -35,11 +35,11 @@ Switch between MAIL and BULLETIN modes? MAIL commands are documented * Database * trigger to limit values for 'visibility'? * Add commands: - * A way to add / delete ssh keys. + * ~~A way to add / delete ssh keys.~~ * A way to manage files? * Commands for a local mail system? * Commands to connect to Mattermost or mastodon? - * Commands to manage users. + * ~~Commands to manage users.~~ * Handle MARK for SELECT and DIRECTORY. Done: diff --git a/ask/ask.go b/ask/ask.go index e8d6a9cdea473f36ff30647f64632eefdfc4f3bc..bdde8a350a9d05ac26cd84870cee0c13fe156890 100644 --- a/ask/ask.go +++ b/ask/ask.go @@ -5,8 +5,10 @@ getting a line of text, getting a choice from a liat and other things. package ask import ( + "errors" "fmt" "os" + "strconv" "github.com/chzyer/readline" ) @@ -29,3 +31,24 @@ func GetLine(prompt string) (string, error) { line, err := rl.Readline() return line, err } + +// Choose presents a list and asks a user to choose one. +func Choose(prompt string, choices []string) (int, error) { + fmt.Println(prompt) + for i := range choices { + fmt.Printf(" %d. %s\n", i+1, choices[i]) + } + response, err := GetLine("Choose a number or enter q to abort: ") + if err != nil { + return -1, err + } + choice, err := strconv.Atoi(response) + if err != nil { + return -1, nil + } + choice-- + if choice < 0 || choice >= len(choices) { + return -1, errors.New("Choice out of bounds") + } + return choice, nil +} diff --git a/key/key.go b/key/key.go index 2e716d95c8c290274ab9221fad92a5b011e4bb7c..634b254dd7e37f714e574e9ce47bbbd1b07b11b8 100644 --- a/key/key.go +++ b/key/key.go @@ -2,24 +2,38 @@ package key import ( + "bufio" + "bytes" + "errors" "fmt" + "io" "os" "path" + "strings" "github.com/adrg/xdg" + "golang.org/x/crypto/ssh" + "golang.org/x/sys/unix" ) -var keytemplate = `command="%s -u %s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding %s\n` +var keytemplate = `command="%s -u %s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding %s` // Add adds an ssh key to the `authorized_keys` file. func Add(login, public string) error { - bulletin, err := os.Executable() + // Parse and verify the key. + public = strings.TrimSpace(public) + theKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) if err != nil { return err } - keyline := fmt.Sprintf(keytemplate, bulletin, login, public) + // Find the bulletin binary. + bulletin, err := os.Executable() + if err != nil { + return err + } + // File system management. sshdir := path.Join(xdg.Home, ".ssh") err = os.MkdirAll(sshdir, 0700) if err != nil { @@ -27,11 +41,28 @@ func Add(login, public string) error { } keyfile := path.Join(sshdir, "authorized_keys") - f, err := os.OpenFile(keyfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + // Open and lock the authorized_keys file. + f, err := os.OpenFile(keyfile, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } defer f.Close() + if err := unix.Flock(int(f.Fd()), unix.LOCK_EX); err != nil { + return err + } + defer unix.Flock(int(f.Fd()), unix.LOCK_UN) // unlock after we're done + + // Check for duplicates. + keycontent, err := io.ReadAll(f) + if err != nil { + return err + } + if bytes.Contains(keycontent, ssh.MarshalAuthorizedKey(theKey)) { + return errors.New("key already exists") + } + + // Generate and write the key. + keyline := fmt.Sprintf(keytemplate, bulletin, login, string(ssh.MarshalAuthorizedKey(theKey))) n, err := f.WriteString(keyline) if err != nil { return err @@ -43,3 +74,119 @@ func Add(login, public string) error { return nil } + +// List returns a list of ssh keys for this user. +func List(login string) ([]string, error) { + keys := []string{} + + // Find the bulletin binary. + bulletin, err := os.Executable() + if err != nil { + return keys, err + } + + // File system management. + sshdir := path.Join(xdg.Home, ".ssh") + err = os.MkdirAll(sshdir, 0700) + if err != nil { + return keys, err + } + keyfile := path.Join(sshdir, "authorized_keys") + + // Open the authorized_keys file. + f, err := os.OpenFile(keyfile, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return keys, err + } + defer f.Close() + + // look for lines. + scanner := bufio.NewScanner(f) + for scanner.Scan() { + keyline := bytes.TrimSpace(scanner.Bytes()) + if len(keyline) == 0 { + continue + } + public, _, options, _, err := ssh.ParseAuthorizedKey([]byte(keyline)) + if err != nil { + return keys, err + } + for i := range options { + opts := strings.SplitN(options[i], "=", 2) + if len(opts) != 2 || opts[0] != "command" { + continue + } + cmd := strings.Split(strings.Trim(opts[1], "\" "), " ") + if len(cmd) != 3 { + return keys, fmt.Errorf("Unexpected command in authorized keys file (%s)", opts[1]) + } + if cmd[0] != bulletin { + return keys, fmt.Errorf("Unexpected bulletin in authorized keys file (%s)", opts[1]) + } + if cmd[1] != "-u" { + return keys, fmt.Errorf("Unexpected flag in authorized keys file (%s)", opts[1]) + } + if cmd[2] == login { + keys = append(keys, strings.Trim(string(ssh.MarshalAuthorizedKey(public)), "\n")) + } + break + } + } + + return keys, nil +} + +// Delete removes the key. +func Delete(public string) error { + keys := []string{} + + // Parse and verify the key. + public = strings.TrimSpace(public) + doomedRaw, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) + if err != nil { + return err + } + + // File system management. + sshdir := path.Join(xdg.Home, ".ssh") + err = os.MkdirAll(sshdir, 0700) + if err != nil { + return err + } + keyfile := path.Join(sshdir, "authorized_keys") + + // Open the authorized_keys file. + f, err := os.OpenFile(keyfile, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return err + } + defer f.Close() + if err := unix.Flock(int(f.Fd()), unix.LOCK_EX); err != nil { + return err + } + defer unix.Flock(int(f.Fd()), unix.LOCK_UN) // unlock after we're done + + // look for lines. + doomed := doomedRaw.Marshal() + scanner := bufio.NewScanner(f) + for scanner.Scan() { + keyline := bytes.TrimSpace(scanner.Bytes()) + if len(keyline) == 0 { + continue + } + potential, _, _, _, err := ssh.ParseAuthorizedKey(keyline) + if err != nil { + return err + } + if bytes.Compare(potential.Marshal(), doomed) != 0 { + keys = append(keys, string(keyline)) + } + } + if _, err := f.Seek(0, os.SEEK_SET); err != nil { + return fmt.Errorf("seek: %w", err) + } + f.Truncate(0) + f.WriteString(strings.Join(keys, "\n") + "\n") + + return nil +} diff --git a/repl/accounts.go b/repl/accounts.go index 0047b875271617cba65bc0c1511a042286bc4cb1..ad8565601606aee6def9ac40212fe251c1cb104d 100644 --- a/repl/accounts.go +++ b/repl/accounts.go @@ -2,8 +2,14 @@ package repl import ( "fmt" + "strings" + "git.lyda.ie/kevin/bulletin/ask" "git.lyda.ie/kevin/bulletin/dclish" + "git.lyda.ie/kevin/bulletin/key" + "git.lyda.ie/kevin/bulletin/storage" + "git.lyda.ie/kevin/bulletin/this" + "git.lyda.ie/kevin/bulletin/users" ) // ActionUser handles the `USER` command. @@ -12,83 +18,272 @@ func ActionUser(cmd *dclish.Command) error { fmt.Println(` The following commands are available: - ADD ADMIN DELETE DISABLE ENABLE MOD - NOADMIN NOMOD`) + ADD ADMIN DELETE DISABLE ENABLE LIST + MOD NOADMIN NOMOD`) fmt.Println() return nil } // ActionUserAdd handles the `USER ADD` command. -func ActionUserAdd(_ *dclish.Command) error { - // TODO: implement +func ActionUserAdd(cmd *dclish.Command) error { + login := strings.ToUpper(cmd.Args[0]) + err := users.ValidLogin(login) + if err != nil { + fmt.Printf("ERROR: %s.\n", err) + return nil + } + return nil +} + +// ActionUserList handles the `USER LIST` command. +func ActionUserList(_ *dclish.Command) error { + if this.User.Admin == 0 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + ctx := storage.Context() + userlist, err := this.Q.ListUsers(ctx) + if err != nil { + fmt.Printf("ERROR: Failed to list users (%s).\n", err) + return nil + } + // TODO: nicer output for user. + for _, u := range userlist { + fmt.Printf("%s\n", u) + } return nil } // ActionUserDelete handles the `USER DELETE` command. -func ActionUserDelete(_ *dclish.Command) error { - // TODO: implement +func ActionUserDelete(cmd *dclish.Command) error { + if this.User.Admin == 0 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + u, err := users.ValidExistingLogin(this.Q, cmd.Args[0]) + if err != nil || u.Login == "" { + fmt.Println("ERROR: User not found.") + return nil + } + ctx := storage.Context() + err = this.Q.DeleteUser(ctx, u.Login) + if err != nil { + fmt.Printf("ERROR: Failed to delete user (%s).\n", err) + return nil + } + fmt.Println("User deleted.") return nil } -// ActionUserEnable handles the `USER ENABLE` command. -func ActionUserEnable(_ *dclish.Command) error { - // TODO: implement +func actionUserEnable(cmd *dclish.Command, disabled int64, doing string) error { + if this.User.Admin == 0 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + u, err := users.ValidExistingLogin(this.Q, cmd.Args[0]) + if err != nil || u.Login == "" { + fmt.Println("ERROR: User not found.") + return nil + } + if u.Disabled == disabled { + fmt.Printf("User already %sd.\n", doing) + return nil + } + ctx := storage.Context() + err = this.Q.UpdateUserDisabled(ctx, storage.UpdateUserDisabledParams{ + Login: u.Login, + Disabled: disabled, + }) + if err != nil { + fmt.Printf("ERROR: Failed to %s user (%s).\n", doing, err) + return nil + } + fmt.Printf("User %sd.\n", doing) return nil } +// ActionUserEnable handles the `USER ENABLE` command. +func ActionUserEnable(cmd *dclish.Command) error { + return actionUserEnable(cmd, 0, "enable") +} + // ActionUserDisable handles the `USER DISABLE` command. -func ActionUserDisable(_ *dclish.Command) error { - // TODO: implement +func ActionUserDisable(cmd *dclish.Command) error { + return actionUserEnable(cmd, 1, "disable") +} + +func actionUserAdmin(cmd *dclish.Command, admin int64, doing string) error { + if this.User.Admin == 0 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + u, err := users.ValidExistingLogin(this.Q, cmd.Args[0]) + if err != nil || u.Login == "" { + fmt.Println("ERROR: User not found.") + return nil + } + if u.Admin == admin { + fmt.Printf("User is already %s.\n", doing) + return nil + } + ctx := storage.Context() + err = this.Q.UpdateUserAdmin(ctx, storage.UpdateUserAdminParams{ + Login: u.Login, + Admin: admin, + }) + if err != nil { + fmt.Printf("ERROR: Failed to make user %s (%s).\n", doing, err) + return nil + } + fmt.Printf("User is now %s.\n", doing) return nil } // ActionUserAdmin handles the `USER ADMIN` command. -func ActionUserAdmin(_ *dclish.Command) error { - // TODO: implement - return nil +func ActionUserAdmin(cmd *dclish.Command) error { + return actionUserAdmin(cmd, 1, "an admin") } // ActionUserNoadmin handles the `USER NOADMIN` command. -func ActionUserNoadmin(_ *dclish.Command) error { - // TODO: implement +func ActionUserNoadmin(cmd *dclish.Command) error { + return actionUserAdmin(cmd, 0, "not an admin") +} + +func actionUserMod(cmd *dclish.Command, mod int64, doing string) error { + if this.User.Admin == 0 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + u, err := users.ValidExistingLogin(this.Q, cmd.Args[0]) + if err != nil || u.Login == "" { + fmt.Println("ERROR: User not found.") + return nil + } + if u.Moderator == mod { + fmt.Printf("User is already %s.\n", doing) + return nil + } + ctx := storage.Context() + err = this.Q.UpdateUserMod(ctx, storage.UpdateUserModParams{ + Login: u.Login, + Moderator: mod, + }) + if err != nil { + fmt.Printf("ERROR: Failed to make user %s (%s).\n", doing, err) + return nil + } + fmt.Printf("User is now %s.\n", doing) return nil } // ActionUserMod handles the `USER MOD` command. -func ActionUserMod(_ *dclish.Command) error { - // TODO: implement - return nil +func ActionUserMod(cmd *dclish.Command) error { + return actionUserMod(cmd, 1, "a moderator") } // ActionUserNomod handles the `USER NOMOD` command. -func ActionUserNomod(_ *dclish.Command) error { - // TODO: implement - return nil +func ActionUserNomod(cmd *dclish.Command) error { + return actionUserMod(cmd, 0, "not a moderator") } // ActionSSH handles the `SSH` command. func ActionSSH(cmd *dclish.Command) error { fmt.Println(cmd.Description) - fmt.Println(`\nThe following commands are available: + fmt.Println(` +The following commands are available: ADD DELETE LIST`) + fmt.Println() return nil } // ActionSSHAdd handles the `SSH ADD` command. -func ActionSSHAdd(_ *dclish.Command) error { - // TODO: implement +func ActionSSHAdd(cmd *dclish.Command) error { + if this.User.Admin == 0 && len(cmd.Args) == 1 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + login := this.User.Login + if len(cmd.Args) == 1 { + login = cmd.Args[0] + } + u, err := users.ValidExistingLogin(this.Q, login) + if err != nil || u.Login == "" { + fmt.Println("ERROR: User not found.") + return nil + } + sshkey, err := ask.GetLine("Enter ssh public key: ") + if err != nil { + fmt.Printf("ERROR: Failed to read ssh key (%s).\n", err) + return nil + } + key.Add(login, sshkey) + fmt.Println("Key is added.") return nil } -// ActionSSHDelete handles the `SSH DELETE` command. -func ActionSSHDelete(_ *dclish.Command) error { - // TODO: implement +// ActionSSHList handles the `SSH LIST` command. +func ActionSSHList(cmd *dclish.Command) error { + if this.User.Admin == 0 && len(cmd.Args) == 1 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + login := this.User.Login + if len(cmd.Args) == 1 { + login = cmd.Args[0] + } + u, err := users.ValidExistingLogin(this.Q, login) + if err != nil || u.Login == "" { + fmt.Println("ERROR: User not found.") + return nil + } + keys, err := key.List(login) + if err != nil { + fmt.Printf("ERROR: Problem listing keys (%s).\n", err) + return nil + } + fmt.Printf("The %d keys:\n %s\n", len(keys), strings.Join(keys, "\n ")) return nil } -// ActionSSHList handles the `SSH LIST` command. -func ActionSSHList(_ *dclish.Command) error { - // TODO: implement +// ActionSSHDelete handles the `SSH DELETE` command. +func ActionSSHDelete(cmd *dclish.Command) error { + if this.User.Admin == 0 && len(cmd.Args) == 1 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + login := this.User.Login + if len(cmd.Args) == 1 { + login = cmd.Args[0] + } + u, err := users.ValidExistingLogin(this.Q, login) + if err != nil || u.Login == "" { + fmt.Println("ERROR: User not found.") + return nil + } + keys, err := key.List(login) + if err != nil { + fmt.Printf("ERROR: Problem listing keys (%s).\n", err) + return nil + } + if len(keys) == 0 { + fmt.Println("No keys to delete.") + return nil + } + choice, err := ask.Choose("Choose a key to delete:", keys) + if err != nil { + fmt.Printf("ERROR: Problem choosing key (%s).\n", err) + return nil + } + if choice < 0 { + fmt.Println("Aborted.") + return nil + } + err = key.Delete(keys[choice]) + if err != nil { + fmt.Printf("ERROR: Problem deleting key (%s).\n", err) + return nil + } + fmt.Println("Key deleted.") return nil } diff --git a/repl/command.go b/repl/command.go index 6b87031f43cc6173c0a93ce3ed0e7e78c4956d2d..493a12b5bd37e8e2de0fe670e0b2f4887be05440 100644 --- a/repl/command.go +++ b/repl/command.go @@ -1141,6 +1141,10 @@ message.`, MaxArgs: 1, Action: ActionUserNoadmin, }, + "LIST": { + Description: ` Lists all the users. Must be an admin.`, + Action: ActionUserList, + }, "MOD": { Description: ` Makes a user an mod.`, MinArgs: 1, @@ -1160,21 +1164,36 @@ message.`, Action: ActionSSH, Commands: dclish.Commands{ "ADD": { - Description: ` Adds an ssh key for a user. + Description: ` Adds an ssh key for a user. Only an admin can add an ssh key for + another user. - Prompts the user for an ssh key and then adds it.`, - Action: ActionSSHAdd, + Prompts the user for an ssh key and then adds it. + + Format: + SSH ADD [login]`, + Action: ActionSSHAdd, + MaxArgs: 1, }, "DELETE": { - Description: ` Removes an ssh key for a user. + Description: ` Removes an ssh key for a user. Only an admin can remove an ssh key for + another user. The user is given a list of current ssh keys and is asked which to - remove.`, - Action: ActionSSHDelete, + remove. + + Format: + SSH DELETE [login]`, + Action: ActionSSHDelete, + MaxArgs: 1, }, "LIST": { - Description: ` Prints a list of ssh keys for the user.`, - Action: ActionSSHList, + Description: ` Prints a list of ssh keys for a user. Only an admin can list ssh keys + for another user. + + Format: + SSH LIST [login]`, + Action: ActionSSHList, + MaxArgs: 1, }, }, }, diff --git a/storage/display.go b/storage/display.go index cf230417b788eb2dc2caf340da33df43db40fbf3..1ac380fb47a2586568427dc019880afcfb0de80d 100644 --- a/storage/display.go +++ b/storage/display.go @@ -36,7 +36,7 @@ func (m *Message) OneLine(expire bool) string { // String displays a user (mainly used for debugging). func (u User) String() string { - return fmt.Sprintf("User %s (%s) [a%d, m%d, !%d, d%d] [%s]", + return fmt.Sprintf("%-12s %-25.25s [a%d, m%d, !%d, d%d] [%s]", u.Login, u.Name, u.Admin, diff --git a/storage/migrations/1_create_table.up.sql b/storage/migrations/1_create_table.up.sql index b2de88bc767f00e431c11bc75a7f6cde00b5b151..46dfee3deadfdb098d29d47f3c288e045e1d5d33 100644 --- a/storage/migrations/1_create_table.up.sql +++ b/storage/migrations/1_create_table.up.sql @@ -106,9 +106,7 @@ CREATE TABLE messages ( folder VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, - author VARCHAR(25) REFERENCES users(login) - ON UPDATE CASCADE - NOT NULL, + author VARCHAR(25) NOT NULL, subject VARCHAR(53) NOT NULL, message TEXT NOT NULL, permanent INT DEFAULT 0 NOT NULL, diff --git a/storage/queries/seed.sql b/storage/queries/seed.sql index 27e3f001b57b0604e444c73cce4049e573549a68..5772eb0de26d41dd8ac541482fa5e0d8927afa0d 100644 --- a/storage/queries/seed.sql +++ b/storage/queries/seed.sql @@ -1,6 +1,6 @@ -- name: SeedUserSystem :exec - INSERT INTO users (login, name, admin) - VALUES ('SYSTEM', 'System User', 1); + INSERT INTO users (login, name, admin, moderator) + VALUES ('SYSTEM', 'System User', 1, 1); -- name: SeedFolderGeneral :exec INSERT INTO folders (name, description, system, shownew) diff --git a/storage/queries/standard.sql b/storage/queries/standard.sql index 3af17721a009c598d416580805560d934cfa0ab5..ca717ed23a539d737ecfbed729874b7b355d331a 100644 --- a/storage/queries/standard.sql +++ b/storage/queries/standard.sql @@ -2,7 +2,7 @@ SELECT * FROM users; -- name: DeleteUser :exec -DELETE FROM users WHERE login = ?; +DELETE FROM users WHERE login = ? AND login != 'SYSTEM'; -- name: AddFolder :exec INSERT INTO folders (name) VALUES (?); diff --git a/storage/queries/users.sql b/storage/queries/users.sql index 1ee85d5ec08441a91b3cb2719d3029f4e796da76..880ddcba85f55b4df96d1a92ba2a24ce176d34e9 100644 --- a/storage/queries/users.sql +++ b/storage/queries/users.sql @@ -7,3 +7,21 @@ RETURNING *; -- name: IsUserAdmin :one SELECT admin FROM users WHERE login = ?; + +-- name: UpdateUserDisabled :exec +UPDATE users SET disabled = ? WHERE login = ? AND login != 'SYSTEM'; + +-- name: UpdateUserAdmin :exec +UPDATE users SET admin = ? WHERE login = ? AND login != 'SYSTEM'; + +-- name: UpdateUserAlert :exec +UPDATE users SET alert = ? WHERE login = ? AND login != 'SYSTEM'; + +-- name: UpdateUserName :exec +UPDATE users SET name = ? WHERE login = ? AND login != 'SYSTEM'; + +-- name: UpdateUserMod :exec +UPDATE users SET moderator = ? WHERE login = ? AND login != 'SYSTEM'; + +-- name: UpdateUserLastLogin :exec +UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE login = ? AND login != 'SYSTEM'; diff --git a/storage/seed.sql.go b/storage/seed.sql.go index c8fcbfd8c7579f354bc914efa6dfa76b6b791551..0f21d68ea537349116887b5424832bb4378128d2 100644 --- a/storage/seed.sql.go +++ b/storage/seed.sql.go @@ -60,8 +60,8 @@ func (q *Queries) SeedGeneralOwner(ctx context.Context) error { } const seedUserSystem = `-- name: SeedUserSystem :exec - INSERT INTO users (login, name, admin) - VALUES ('SYSTEM', 'System User', 1) + INSERT INTO users (login, name, admin, moderator) + VALUES ('SYSTEM', 'System User', 1, 1) ` func (q *Queries) SeedUserSystem(ctx context.Context) error { diff --git a/storage/standard.sql.go b/storage/standard.sql.go index 8ddd2c88551047c5fa34cfd1f88ac678d9501e57..edf7270080cc965d00f6d403f128dc5c322cfabf 100644 --- a/storage/standard.sql.go +++ b/storage/standard.sql.go @@ -209,7 +209,7 @@ func (q *Queries) DeleteSeen(ctx context.Context, arg DeleteSeenParams) error { } const deleteUser = `-- name: DeleteUser :exec -DELETE FROM users WHERE login = ? +DELETE FROM users WHERE login = ? AND login != 'SYSTEM' ` func (q *Queries) DeleteUser(ctx context.Context, login string) error { diff --git a/storage/users.sql.go b/storage/users.sql.go index ae1cad7f0b6854991acbb15bff8a2d00538b05f8..79a138ade0b0731243387eec91bf49af551f3ddd 100644 --- a/storage/users.sql.go +++ b/storage/users.sql.go @@ -68,3 +68,82 @@ func (q *Queries) IsUserAdmin(ctx context.Context, login string) (int64, error) err := row.Scan(&admin) return admin, err } + +const updateUserAdmin = `-- name: UpdateUserAdmin :exec +UPDATE users SET admin = ? WHERE login = ? AND login != 'SYSTEM' +` + +type UpdateUserAdminParams struct { + Admin int64 + Login string +} + +func (q *Queries) UpdateUserAdmin(ctx context.Context, arg UpdateUserAdminParams) error { + _, err := q.db.ExecContext(ctx, updateUserAdmin, arg.Admin, arg.Login) + return err +} + +const updateUserAlert = `-- name: UpdateUserAlert :exec +UPDATE users SET alert = ? WHERE login = ? AND login != 'SYSTEM' +` + +type UpdateUserAlertParams struct { + Alert int64 + Login string +} + +func (q *Queries) UpdateUserAlert(ctx context.Context, arg UpdateUserAlertParams) error { + _, err := q.db.ExecContext(ctx, updateUserAlert, arg.Alert, arg.Login) + return err +} + +const updateUserDisabled = `-- name: UpdateUserDisabled :exec +UPDATE users SET disabled = ? WHERE login = ? AND login != 'SYSTEM' +` + +type UpdateUserDisabledParams struct { + Disabled int64 + Login string +} + +func (q *Queries) UpdateUserDisabled(ctx context.Context, arg UpdateUserDisabledParams) error { + _, err := q.db.ExecContext(ctx, updateUserDisabled, arg.Disabled, arg.Login) + return err +} + +const updateUserLastLogin = `-- name: UpdateUserLastLogin :exec +UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE login = ? AND login != 'SYSTEM' +` + +func (q *Queries) UpdateUserLastLogin(ctx context.Context, login string) error { + _, err := q.db.ExecContext(ctx, updateUserLastLogin, login) + return err +} + +const updateUserMod = `-- name: UpdateUserMod :exec +UPDATE users SET moderator = ? WHERE login = ? AND login != 'SYSTEM' +` + +type UpdateUserModParams struct { + Moderator int64 + Login string +} + +func (q *Queries) UpdateUserMod(ctx context.Context, arg UpdateUserModParams) error { + _, err := q.db.ExecContext(ctx, updateUserMod, arg.Moderator, arg.Login) + return err +} + +const updateUserName = `-- name: UpdateUserName :exec +UPDATE users SET name = ? WHERE login = ? AND login != 'SYSTEM' +` + +type UpdateUserNameParams struct { + Name string + Login string +} + +func (q *Queries) UpdateUserName(ctx context.Context, arg UpdateUserNameParams) error { + _, err := q.db.ExecContext(ctx, updateUserName, arg.Name, arg.Login) + return err +} diff --git a/users/users.go b/users/users.go index ecbf8ef13c05340070ec0e8ad23ce9ab49c27c84..bf3da42446a4a2f2d11b19de6e5ecd510e8737aa 100644 --- a/users/users.go +++ b/users/users.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "git.lyda.ie/kevin/bulletin/storage" _ "modernc.org/sqlite" // Loads sqlite driver. ) @@ -14,8 +15,22 @@ func ValidLogin(login string) error { if login == "" { return errors.New("empty account is invalid") } - if strings.ContainsAny(login, "./") { + if strings.ContainsAny(login, "./%:") { return fmt.Errorf("account name '%s' is invalid", login) } return nil } + +// ValidExistingLogin makes sure that an account name is a valid name. +func ValidExistingLogin(q *storage.Queries, login string) (storage.User, error) { + u := storage.User{} + if login == "" { + return u, errors.New("empty account is invalid") + } + login = strings.ToUpper(login) + if strings.ContainsAny(login, "./%:") { + return u, fmt.Errorf("account name '%s' is invalid", login) + } + ctx := storage.Context() + return q.GetUser(ctx, login) +}