From a415178d0521f7135be8bf7f0082b67be8110a0b Mon Sep 17 00:00:00 2001 From: Kevin Lyda <kevin@lyda.ie> Date: Sat, 17 May 2025 13:33:37 +0100 Subject: [PATCH] Implement the SSH and USER commands --- NOTES.md | 4 +- ask/ask.go | 23 ++ key/key.go | 155 +++++++++++++- repl/accounts.go | 257 ++++++++++++++++++++--- repl/command.go | 35 ++- storage/display.go | 2 +- storage/migrations/1_create_table.up.sql | 4 +- storage/queries/seed.sql | 4 +- storage/queries/standard.sql | 2 +- storage/queries/users.sql | 18 ++ storage/seed.sql.go | 4 +- storage/standard.sql.go | 2 +- storage/users.sql.go | 79 +++++++ users/users.go | 17 +- 14 files changed, 550 insertions(+), 56 deletions(-) diff --git a/NOTES.md b/NOTES.md index 7651e96..4d60cce 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 e8d6a9c..bdde8a3 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 2e716d9..634b254 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 0047b87..ad85656 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 6b87031..493a12b 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 cf23041..1ac380f 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 b2de88b..46dfee3 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 27e3f00..5772eb0 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 3af1772..ca717ed 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 1ee85d5..880ddcb 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 c8fcbfd..0f21d68 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 8ddd2c8..edf7270 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 ae1cad7..79a138a 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 ecbf8ef..bf3da42 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) +} -- GitLab