From d52eac0948f1c7210dd6e71606b17fd79f63f6fa Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Mon, 24 Jun 2024 00:08:37 +0530 Subject: [PATCH] Update user APIs and queries to embed role + list permissions. --- frontend/src/main.js | 4 +- frontend/src/views/UserForm.vue | 2 +- frontend/src/views/Users.vue | 4 +- internal/core/users.go | 94 ++++++++++++++++++++------------- models/models.go | 36 ++++++++----- models/queries.go | 1 - queries.sql | 33 ++++++++---- 7 files changed, 108 insertions(+), 66 deletions(-) diff --git a/frontend/src/main.js b/frontend/src/main.js index 475c25dd..eb033bd7 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -54,10 +54,10 @@ async function initConfig(app) { // one of campaigns:get, campaigns:manage etc. are present. if (perm.endsWith('*')) { const group = `${perm.split(':')[0]}:`; - return profile.permissions.some((p) => p.startsWith(group)); + return profile.role.permissions.some((p) => p.startsWith(group)); } - return profile.permissions.includes(perm); + return profile.role.permissions.includes(perm); }; // Set the page title after i18n has loaded. diff --git a/frontend/src/views/UserForm.vue b/frontend/src/views/UserForm.vue index 6ff23435..60bc1a4d 100644 --- a/frontend/src/views/UserForm.vue +++ b/frontend/src/views/UserForm.vue @@ -198,7 +198,7 @@ export default Vue.extend({ }, mounted() { - this.form = { ...this.form, ...this.$props.data }; + this.form = { ...this.form, ...this.$props.data, roleId: this.$props.data.role.id }; this.$api.getRoles(); diff --git a/frontend/src/views/Users.vue b/frontend/src/views/Users.vue index 47fc33e4..3f055d48 100644 --- a/frontend/src/views/Users.vue +++ b/frontend/src/views/Users.vue @@ -52,8 +52,8 @@ - - {{ props.row.roleName }} + + {{ props.row.role.name }} diff --git a/internal/core/users.go b/internal/core/users.go index e6af6f2e..f46de061 100644 --- a/internal/core/users.go +++ b/internal/core/users.go @@ -2,6 +2,7 @@ package core import ( "database/sql" + "encoding/json" "net/http" "github.com/knadh/listmonk/internal/utils" @@ -13,47 +14,19 @@ import ( // GetUsers retrieves all users. func (c *Core) GetUsers() ([]models.User, error) { - out := []models.User{} - if err := c.q.GetUsers.Select(&out, 0); err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, - c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err))) - } - - for n, u := range out { - if u.Password.String != "" { - u.HasPassword = true - u.PasswordLogin = true - // u.Password = null.String{} - - out[n] = u - } - - if u.Type == models.UserTypeAPI { - out[n].Email = null.String{} - } - } - - return out, nil + out, err := c.getUsers(0, "", "") + return out, err } // GetUser retrieves a specific user based on any one given identifier. func (c *Core) GetUser(id int, username, email string) (models.User, error) { - var out models.User - if err := c.q.GetUser.Get(&out, id, username, email); err != nil { - return out, echo.NewHTTPError(http.StatusInternalServerError, + out, err := c.getUsers(id, username, email) + if err != nil { + return models.User{}, echo.NewHTTPError(http.StatusInternalServerError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err))) } - if out.Password.String != "" { - out.HasPassword = true - out.PasswordLogin = true - } - out.PermissionsMap = make(map[string]struct{}) - for _, p := range out.Permissions { - out.PermissionsMap[p] = struct{}{} - } - - return out, nil + return out[0], nil } // CreateUser creates a new user. @@ -146,9 +119,56 @@ func (c *Core) LoginUser(username, password string) (models.User, error) { c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err))) } - out.PermissionsMap = make(map[string]struct{}) - for _, p := range out.Permissions { - out.PermissionsMap[p] = struct{}{} + return out, nil +} + +func (c *Core) getUsers(id int, username, email string) ([]models.User, error) { + out := []models.User{} + if err := c.q.GetUsers.Select(&out, id, username, email); err != nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, + c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err))) + } + + for n, u := range out { + if u.Password.String != "" { + u.HasPassword = true + u.PasswordLogin = true + } + + if u.Type == models.UserTypeAPI { + u.Email = null.String{} + } + + // Unmarshall the raw list perms map. + var listPerms []models.ListPermission + if u.ListsPermsRaw != nil { + if err := json.Unmarshal(u.ListsPermsRaw, &listPerms); err != nil { + c.log.Printf("error unmarshalling list permissions for role %d: %v", u.ID, err) + } + } + + u.Role.ID = u.RoleID + u.Role.Name = u.RoleName + u.Role.Permissions = u.RolePerms + u.Role.Lists = listPerms + u.RoleID = 0 + + // Prepare lookup maps. + u.PermissionsMap = make(map[string]struct{}) + for _, p := range u.RolePerms { + u.PermissionsMap[p] = struct{}{} + } + + u.ListPermissionsMap = make(map[int]map[string]struct{}) + for _, p := range listPerms { + u.ListPermissionsMap[p.ID] = make(map[string]struct{}) + + for _, perm := range p.Permissions { + u.ListPermissionsMap[p.ID][perm] = struct{}{} + } + } + + out[n] = u } return out, nil diff --git a/models/models.go b/models/models.go index 23188d28..1e9b1e77 100644 --- a/models/models.go +++ b/models/models.go @@ -151,21 +151,31 @@ type User struct { Username string `db:"username" json:"username"` // For API users, this is the plaintext API token. - Password null.String `db:"password" json:"password,omitempty"` - PasswordLogin bool `db:"password_login" json:"password_login"` - Email null.String `db:"email" json:"email"` - Name string `db:"name" json:"name"` - Type string `db:"type" json:"type"` - RoleID int `db:"role_id" json:"role_id"` - RoleName string `db:"role_name" json:"role_name"` - Permissions pq.StringArray `db:"permissions" json:"permissions"` - Status string `db:"status" json:"status"` - Avatar null.String `db:"-" json:"avatar"` + Password null.String `db:"password" json:"password,omitempty"` + PasswordLogin bool `db:"password_login" json:"password_login"` + Email null.String `db:"email" json:"email"` + Name string `db:"name" json:"name"` + Type string `db:"type" json:"type"` + Status string `db:"status" json:"status"` + Avatar null.String `db:"-" json:"avatar"` + LoggedInAt null.Time `db:"loggedin_at" json:"loggedin_at"` - PermissionsMap map[string]struct{} `db:"-" json:"-"` - LoggedInAt null.Time `db:"loggedin_at" json:"loggedin_at"` + // Filled post-retrieval. + Role struct { + ID int `db:"-" json:"id"` + Name string `db:"-" json:"name"` + Permissions []string `db:"-" json:"permissions"` + Lists []ListPermission `db:"-" json:"lists"` + } `db:"-" json:"role"` - HasPassword bool `db:"-" json:"-"` + RoleID int `db:"role_id" json:"role_id,omitempty"` + RoleName string `db:"role_name" json:"-"` + RolePerms pq.StringArray `db:"role_permissions" json:"-"` + ListsPermsRaw json.RawMessage `db:"list_permissions" json:"-"` + + PermissionsMap map[string]struct{} `db:"-" json:"-"` + ListPermissionsMap map[int]map[string]struct{} `db:"-" json:"-"` + HasPassword bool `db:"-" json:"-"` } type ListPermission struct { diff --git a/models/queries.go b/models/queries.go index 2a52478d..7b17cf19 100644 --- a/models/queries.go +++ b/models/queries.go @@ -113,7 +113,6 @@ type Queries struct { UpdateUserProfile *sqlx.Stmt `query:"update-user-profile"` DeleteUsers *sqlx.Stmt `query:"delete-users"` GetUsers *sqlx.Stmt `query:"get-users"` - GetUser *sqlx.Stmt `query:"get-user"` GetAPITokens *sqlx.Stmt `query:"get-api-tokens"` LoginUser *sqlx.Stmt `query:"login-user"` diff --git a/queries.sql b/queries.sql index 8188b3fd..34e86272 100644 --- a/queries.sql +++ b/queries.sql @@ -1062,22 +1062,35 @@ WITH u AS ( ) DELETE FROM users WHERE id = ALL($1) AND (SELECT num FROM u) > 0; --- name: get-user -SELECT users.*, r.name as role_name, r.permissions FROM users - LEFT JOIN user_roles r ON (r.id = users.role_id) +-- name: get-users +WITH u AS ( + SELECT * FROM users WHERE ( CASE WHEN $1::INT != 0 THEN users.id = $1 WHEN $2::TEXT != '' THEN username = $2 WHEN $3::TEXT != '' THEN email = $3 + ELSE TRUE END - ); + ) +), +role AS ( + SELECT id, name, permissions FROM user_roles WHERE id IN (SELECT role_id FROM users) +), +listPerms AS ( + SELECT ur.parent_id, JSONB_AGG(JSONB_BUILD_OBJECT('id', ur.list_id, 'name', lists.name, 'permissions', ur.permissions)) AS listPerms + FROM user_roles ur + LEFT JOIN lists ON(lists.id = ur.list_id) + WHERE ur.parent_id IS NOT NULL GROUP BY ur.parent_id +), +roleInfo AS ( + SELECT role.id AS role_id, role.name AS role_name, role.permissions AS role_permissions, COALESCE(l.listPerms, '[]'::JSONB) AS "list_permissions" + FROM role + LEFT JOIN listPerms l ON role.id = l.parent_id +) +SELECT u.*, ri.* FROM u JOIN roleInfo ri ON u.role_id = ri.role_id; --- name: get-users -SELECT users.*, r.name as role_name, r.permissions FROM users - LEFT JOIN user_roles r ON (r.id = users.role_id) - WHERE $1=0 OR users.id=$1 ORDER BY created_at; -- name: get-api-tokens SELECT username, password FROM users WHERE status='enabled' AND type='api'; @@ -1099,14 +1112,14 @@ UPDATE users SET name=$2, email=$3, WITH mainroles AS ( SELECT ur.* FROM user_roles ur WHERE ur.parent_id IS NULL ), -listroles AS ( +listPerms AS ( SELECT ur.parent_id, JSONB_AGG(JSONB_BUILD_OBJECT('id', ur.list_id, 'name', lists.name, 'permissions', ur.permissions)) AS listPerms FROM user_roles ur LEFT JOIN lists ON(lists.id = ur.list_id) WHERE ur.parent_id IS NOT NULL GROUP BY ur.parent_id ) SELECT p.*, COALESCE(l.listPerms, '[]'::JSONB) AS "list_permissions" FROM mainroles p - LEFT JOIN listroles l ON p.id = l.parent_id; + LEFT JOIN listPerms l ON p.id = l.parent_id; -- name: create-role INSERT INTO user_roles (name, permissions, created_at, updated_at) VALUES($1, $2, NOW(), NOW()) RETURNING *;