diff --git a/cmd/handlers.go b/cmd/handlers.go index 4a6ec67b..b630ab1c 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -206,9 +206,12 @@ func initHTTPHandlers(e *echo.Echo, app *App) { api.DELETE("/api/users/:id", pm(handleDeleteUsers, "users:manage")) api.POST("/api/logout", handleLogout) - api.GET("/api/roles", pm(handleGetRoles, "roles:get")) - api.POST("/api/roles", pm(handleCreateRole, "roles:manage")) - api.PUT("/api/roles/:id", pm(handleUpdateRole, "roles:manage")) + api.GET("/api/roles/users", pm(handleGetUserRoles, "roles:get")) + api.GET("/api/roles/lists", pm(handleGeListRoles, "roles:get")) + api.POST("/api/roles/users", pm(handleCreateUserRole, "roles:manage")) + api.POST("/api/roles/lists", pm(handleCreateListRole, "roles:manage")) + api.PUT("/api/roles/users/:id", pm(handleUpdateUserRole, "roles:manage")) + api.PUT("/api/roles/lists/:id", pm(handleUpdateListRole, "roles:manage")) api.DELETE("/api/roles/:id", pm(handleDeleteRole, "roles:manage")) if app.constants.BounceWebhooksEnabled { diff --git a/cmd/init.go b/cmd/init.go index b4e4dc3f..1b5d9de1 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -998,7 +998,7 @@ func initAuth(db *sql.DB, ko *koanf.Koanf, co *core.Core) *auth.Auth { Status: models.UserStatusEnabled, Type: models.UserTypeAPI, } - u.Role.ID = auth.SuperAdminRoleID + u.UserRole.ID = auth.SuperAdminRoleID a.CacheAPIUser(u) lo.Println(`WARNING: Remove the admin_username and admin_password fields from the TOML configuration file. If you are using APIs, create and use new credentials. Users are now managed via the Admin -> Settings -> Users dashboard.`) diff --git a/cmd/install.go b/cmd/install.go index 0284181d..58fc9f27 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -108,7 +108,7 @@ func installUser(q *models.Queries) (string, string) { perms = append(perms, p) } - if _, err := q.CreateRole.Exec("Super Admin", pq.Array(perms)); err != nil { + if _, err := q.CreateRole.Exec("Super Admin", "user", pq.Array(perms)); err != nil { lo.Fatalf("error creating super admin role: %v", err) } @@ -146,7 +146,7 @@ func installUser(q *models.Queries) (string, string) { lo.Printf("creating admin user '%s'. Credential source is '%s'", user, typ) - if _, err := q.CreateUser.Exec(user, true, password, user+"@listmonk", user, "user", 1, "enabled"); err != nil { + if _, err := q.CreateUser.Exec(user, true, password, user+"@listmonk", user, "user", 1, nil, "enabled"); err != nil { lo.Fatalf("error creating superadmin user: %v", err) } diff --git a/cmd/roles.go b/cmd/roles.go index 9afb9d8e..cf193cdc 100644 --- a/cmd/roles.go +++ b/cmd/roles.go @@ -10,8 +10,8 @@ import ( "github.com/labstack/echo/v4" ) -// handleGetRoles retrieves roles. -func handleGetRoles(c echo.Context) error { +// handleGetUserRoles retrieves roles. +func handleGetUserRoles(c echo.Context) error { var ( app = c.Get("app").(*App) ) @@ -25,8 +25,23 @@ func handleGetRoles(c echo.Context) error { return c.JSON(http.StatusOK, okResp{out}) } -// handleCreateRole handles role creation. -func handleCreateRole(c echo.Context) error { +// handleGeListRoles retrieves roles. +func handleGeListRoles(c echo.Context) error { + var ( + app = c.Get("app").(*App) + ) + + // Get all roles. + out, err := app.core.GetListRoles() + if err != nil { + return err + } + + return c.JSON(http.StatusOK, okResp{out}) +} + +// handleCreateUserRole handles role creation. +func handleCreateUserRole(c echo.Context) error { var ( app = c.Get("app").(*App) r = models.Role{} @@ -36,7 +51,7 @@ func handleCreateRole(c echo.Context) error { return err } - if err := validateRole(r, app); err != nil { + if err := validateUserRole(r, app); err != nil { return err } @@ -48,8 +63,31 @@ func handleCreateRole(c echo.Context) error { return c.JSON(http.StatusOK, okResp{out}) } -// handleUpdateRole handles role modification. -func handleUpdateRole(c echo.Context) error { +// handleCreateListRole handles role creation. +func handleCreateListRole(c echo.Context) error { + var ( + app = c.Get("app").(*App) + r = models.ListRole{} + ) + + if err := c.Bind(&r); err != nil { + return err + } + + if err := validateListRole(r, app); err != nil { + return err + } + + out, err := app.core.CreateListRole(r) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, okResp{out}) +} + +// handleUpdateUserRole handles role modification. +func handleUpdateUserRole(c echo.Context) error { var ( app = c.Get("app").(*App) id, _ = strconv.Atoi(c.Param("id")) @@ -65,14 +103,51 @@ func handleUpdateRole(c echo.Context) error { return err } - if err := validateRole(r, app); err != nil { + if err := validateUserRole(r, app); err != nil { return err } // Validate. r.Name.String = strings.TrimSpace(r.Name.String) - out, err := app.core.UpdateRole(id, r) + out, err := app.core.UpdateUserRole(id, r) + if err != nil { + return err + } + + // Cache the API token for validating API queries without hitting the DB every time. + if err := cacheAPIUsers(app.core, app.auth); err != nil { + return err + } + + return c.JSON(http.StatusOK, okResp{out}) +} + +// handleUpdateListRole handles role modification. +func handleUpdateListRole(c echo.Context) error { + var ( + app = c.Get("app").(*App) + id, _ = strconv.Atoi(c.Param("id")) + ) + + if id < 2 { + return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID")) + } + + // Incoming params. + var r models.ListRole + if err := c.Bind(&r); err != nil { + return err + } + + if err := validateListRole(r, app); err != nil { + return err + } + + // Validate. + r.Name.String = strings.TrimSpace(r.Name.String) + + out, err := app.core.UpdateListRole(id, r) if err != nil { return err } @@ -108,9 +183,9 @@ func handleDeleteRole(c echo.Context) error { return c.JSON(http.StatusOK, okResp{true}) } -func validateRole(r models.Role, app *App) error { +func validateUserRole(r models.Role, app *App) error { // Validate fields. - if !strHasLen(r.Name.String, 2, stdInputMaxLen) { + if !strHasLen(r.Name.String, 1, stdInputMaxLen) { return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "name")) } @@ -120,6 +195,15 @@ func validateRole(r models.Role, app *App) error { } } + return nil +} + +func validateListRole(r models.ListRole, app *App) error { + // Validate fields. + if !strHasLen(r.Name.String, 1, stdInputMaxLen) { + return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "name")) + } + for _, l := range r.Lists { for _, p := range l.Permissions { if p != "list:get" && p != "list:manage" { diff --git a/cmd/subscribers.go b/cmd/subscribers.go index bc801d93..8907c436 100644 --- a/cmd/subscribers.go +++ b/cmd/subscribers.go @@ -665,7 +665,7 @@ func sendOptinConfirmationHook(app *App) func(sub models.Subscriber, listIDs []i // hasSubPerm checks whether the current user has permission to access the given list // of subscriber IDs. func hasSubPerm(u models.User, subIDs []int, app *App) error { - if u.RoleID == auth.SuperAdminRoleID { + if u.UserRoleID == auth.SuperAdminRoleID { return nil } diff --git a/cmd/users.go b/cmd/users.go index cab08beb..f01f99c4 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -261,7 +261,7 @@ func handleUpdateUserProfile(c echo.Context) error { } } - out, err := app.core.UpdateUser(user.ID, u) + out, err := app.core.UpdateUserProfile(user.ID, u) if err != nil { return err } diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 777a5ef6..5f4f11a6 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -489,24 +489,41 @@ export const updateUserProfile = (data) => http.put( { loading: models.users, store: models.profile }, ); -export const getRoles = async () => http.get( - '/api/roles', - { loading: models.roles, store: models.roles }, +export const getUserRoles = async () => http.get( + '/api/roles/users', + { loading: models.userRoles, store: models.userRoles }, ); -export const createRole = (data) => http.post( - '/api/roles', - data, - { loading: models.roles }, +export const getListRoles = async () => http.get( + '/api/roles/lists', + { loading: models.listRoles, store: models.listRoles }, ); -export const updateRole = (data) => http.put( - `/api/roles/${data.id}`, +export const createUserRole = (data) => http.post( + '/api/roles/users', data, - { loading: models.roles }, + { loading: models.userRoles }, +); + +export const createListRole = (data) => http.post( + '/api/roles/lists', + data, + { loading: models.listRoles }, +); + +export const updateUserRole = (data) => http.put( + `/api/roles/users/${data.id}`, + data, + { loading: models.userRoles }, +); + +export const updateListRole = (data) => http.put( + `/api/roles/lists/${data.id}`, + data, + { loading: models.userRoles }, ); export const deleteRole = (id) => http.delete( `/api/roles/${id}`, - { loading: models.roles }, + { loading: models.userRoles }, ); diff --git a/frontend/src/components/Navigation.vue b/frontend/src/components/Navigation.vue index 4c229f1e..340769a8 100644 --- a/frontend/src/components/Navigation.vue +++ b/frontend/src/components/Navigation.vue @@ -42,15 +42,22 @@ :label="$t('globals.terms.analytics')" /> + + + + + + - - { - if (profile.role_id === 1) { + if (profile.userRole.id === 1) { return true; } @@ -55,10 +55,10 @@ async function initConfig(app) { return perms.some((perm) => { if (perm.endsWith('*')) { const group = `${perm.split(':')[0]}:`; - return profile.role.permissions.some((p) => p.startsWith(group)); + return profile.userRole.permissions.some((p) => p.startsWith(group)); } - return profile.role.permissions.includes(perm); + return profile.userRole.permissions.includes(perm); }); }; diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index f28d378f..2d6f33df 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -114,15 +114,21 @@ const routes = [ component: () => import('../views/Logs.vue'), }, { - path: '/settings/users', + path: '/users', name: 'users', - meta: { title: 'globals.terms.users', group: 'settings' }, + meta: { title: 'globals.terms.users', group: 'users' }, component: () => import('../views/Users.vue'), }, { - path: '/settings/users/roles', - name: 'roles', - meta: { title: 'users.roles', group: 'settings' }, + path: '/users/roles/users', + name: 'userRoles', + meta: { title: 'users.userRoles', group: 'users' }, + component: () => import('../views/Roles.vue'), + }, + { + path: '/users/roles/lists', + name: 'listRoles', + meta: { title: 'users.listRoles', group: 'users' }, component: () => import('../views/Roles.vue'), }, { diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 1398a938..a5bfeb3d 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -43,7 +43,8 @@ export default new Vuex.Store({ [models.templates]: (state) => state[models.templates], [models.users]: (state) => state[models.users], [models.profile]: (state) => state[models.profile], - [models.roles]: (state) => state[models.roles], + [models.userRoles]: (state) => state[models.userRoles], + [models.listRoles]: (state) => state[models.listRoles], [models.settings]: (state) => state[models.settings], [models.serverConfig]: (state) => state[models.serverConfig], [models.logs]: (state) => state[models.logs], diff --git a/frontend/src/views/ListForm.vue b/frontend/src/views/ListForm.vue index e0002883..199597df 100644 --- a/frontend/src/views/ListForm.vue +++ b/frontend/src/views/ListForm.vue @@ -130,7 +130,7 @@ export default Vue.extend({ return true; } - const list = this.profile.role.lists.find((l) => l.id === this.$props.data.id); + const list = this.profile.userRole.lists.find((l) => l.id === this.$props.data.id); return list && list.permissions.includes('list:manage'); }, }, diff --git a/frontend/src/views/RoleForm.vue b/frontend/src/views/RoleForm.vue index ee68e857..d39b10d3 100644 --- a/frontend/src/views/RoleForm.vue +++ b/frontend/src/views/RoleForm.vue @@ -9,7 +9,7 @@ {{ data.name }}

- {{ $t('users.newRole') }} + {{ type === 'user' ? $t('users.newUserRole') : $t('users.newListRole') }}

@@ -19,13 +19,13 @@ required /> -
+
{{ $t('users.listPerms') }}
+ :disabled="disabled || filteredLists.length < 1" expanded class="mb-3">