mirror of
https://github.com/knadh/listmonk.git
synced 2025-09-27 08:54:56 +08:00
Add support for "list roles".
This commit splits roles into two, user roles and list roles, both of which are attached separately to a user. List roles are collection of lists each with read|write permissions, while user roles now have all permissions except for per-list ones. This allows for easier management of roles, eliminating the need to clone and create new roles just to adjust specific list permissions.
This commit is contained in:
parent
12a6451ed0
commit
ae2a386193
26 changed files with 625 additions and 246 deletions
|
@ -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 {
|
||||
|
|
|
@ -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.`)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
106
cmd/roles.go
106
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" {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
);
|
||||
|
|
|
@ -42,15 +42,22 @@
|
|||
:label="$t('globals.terms.analytics')" />
|
||||
</b-menu-item><!-- campaigns -->
|
||||
|
||||
<b-menu-item v-if="$can('users:*', 'roles:*')" :expanded="activeGroup.users" :active="activeGroup.users"
|
||||
data-cy="users" @update:active="(state) => toggleGroup('users', state)" icon="account-multiple"
|
||||
:label="$t('globals.terms.users')">
|
||||
<b-menu-item v-if="$can('users:get')" :to="{ name: 'users' }" tag="router-link" :active="activeItem.users"
|
||||
data-cy="users" icon="account-multiple" :label="$t('globals.terms.users')" />
|
||||
<b-menu-item v-if="$can('roles:get')" :to="{ name: 'userRoles' }" tag="router-link" :active="activeItem.userRoles"
|
||||
data-cy="userRoles" icon="newspaper-variant-outline" :label="$t('users.userRoles')" />
|
||||
<b-menu-item v-if="$can('roles:get')" :to="{ name: 'listRoles' }" tag="router-link" :active="activeItem.listRoles"
|
||||
data-cy="listRoles" icon="format-list-bulleted-square" :label="$t('users.listRoles')" />
|
||||
</b-menu-item><!-- users -->
|
||||
|
||||
<b-menu-item v-if="$can('settings:*')" :expanded="activeGroup.settings" :active="activeGroup.settings"
|
||||
data-cy="settings" @update:active="(state) => toggleGroup('settings', state)" icon="cog-outline"
|
||||
:label="$t('menu.settings')">
|
||||
<b-menu-item v-if="$can('settings:get')" :to="{ name: 'settings' }" tag="router-link"
|
||||
:active="activeItem.settings" data-cy="all-settings" icon="cog-outline" :label="$t('menu.settings')" />
|
||||
<b-menu-item v-if="$can('users:get')" :to="{ name: 'users' }" tag="router-link" :active="activeItem.users"
|
||||
data-cy="users" icon="account-multiple" :label="$t('globals.terms.users')" />
|
||||
<b-menu-item v-if="$can('roles:get')" :to="{ name: 'roles' }" tag="router-link" :active="activeItem.roles"
|
||||
data-cy="roles" icon="newspaper-variant-outline" :label="$t('users.roles')" />
|
||||
<b-menu-item v-if="$can('settings:maintain')" :to="{ name: 'maintenance' }" tag="router-link"
|
||||
:active="activeItem.maintenance" data-cy="maintenance" icon="wrench-outline" :label="$t('menu.maintenance')" />
|
||||
<b-menu-item v-if="$can('settings:get')" :to="{ name: 'logs' }" tag="router-link" :active="activeItem.logs"
|
||||
|
|
|
@ -10,7 +10,8 @@ export const models = Object.freeze({
|
|||
bounces: 'bounces',
|
||||
users: 'users',
|
||||
profile: 'profile',
|
||||
roles: 'roles',
|
||||
userRoles: 'userRoles',
|
||||
listRoles: 'listRoles',
|
||||
settings: 'settings',
|
||||
logs: 'logs',
|
||||
maintenance: 'maintenance',
|
||||
|
|
|
@ -45,7 +45,7 @@ async function initConfig(app) {
|
|||
// $can('permission:name') is used in the UI to chekc whether the logged in user
|
||||
// has a certain permission to toggle visibility of UI objects and UI functionality.
|
||||
Vue.prototype.$can = (...perms) => {
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{{ data.name }}
|
||||
</h4>
|
||||
<h4 v-else>
|
||||
{{ $t('users.newRole') }}
|
||||
{{ type === 'user' ? $t('users.newUserRole') : $t('users.newListRole') }}
|
||||
</h4>
|
||||
</header>
|
||||
|
||||
|
@ -19,13 +19,13 @@
|
|||
required />
|
||||
</b-field>
|
||||
|
||||
<div v-if="!disabled" class="box">
|
||||
<div v-if="type === 'list'" class="box">
|
||||
<h5>{{ $t('users.listPerms') }}</h5>
|
||||
<div class="mb-5">
|
||||
<div class="columns">
|
||||
<div class="column is-9">
|
||||
<b-select :placeholder="$tc('globals.terms.list')" v-model="form.curList" name="list"
|
||||
:disabled="disabled" expanded class="mb-3">
|
||||
:disabled="disabled || filteredLists.length < 1" expanded class="mb-3">
|
||||
<template v-for="l in filteredLists">
|
||||
<option :value="l.id" :key="l.id">
|
||||
{{ l.name }}
|
||||
|
@ -74,7 +74,7 @@
|
|||
</b-table>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<template v-if="type === 'user'">
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<h5 class="mb-0">
|
||||
|
@ -99,7 +99,7 @@
|
|||
</div>
|
||||
</b-table-column>
|
||||
</b-table>
|
||||
</div>
|
||||
</template>
|
||||
<a href="https://listmonk.app/docs/roles-and-permissions" target="_blank" rel="noopener noreferrer">
|
||||
<b-icon icon="link-variant" /> {{ $t('globals.buttons.learnMore') }}
|
||||
</a>
|
||||
|
@ -132,6 +132,7 @@ export default Vue.extend({
|
|||
props: {
|
||||
data: { type: Object, default: () => ({}) },
|
||||
isEditing: { type: Boolean, default: false },
|
||||
type: { type: String, default: 'user' },
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -186,13 +187,21 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
createRole() {
|
||||
const lists = this.form.lists.reduce((acc, item) => {
|
||||
acc.push({ id: item.id, permissions: item.permissions });
|
||||
return acc;
|
||||
}, []);
|
||||
let fn;
|
||||
const form = { name: this.form.name };
|
||||
|
||||
const form = { name: this.form.name, permissions: this.form.permissions, lists };
|
||||
this.$api.createRole(form).then((data) => {
|
||||
if (this.$props.type === 'user') {
|
||||
fn = this.$api.createUserRole;
|
||||
form.permissions = this.form.permissions;
|
||||
} else {
|
||||
fn = this.$api.createListRole;
|
||||
form.lists = this.form.lists.reduce((acc, item) => {
|
||||
acc.push({ id: item.id, permissions: item.permissions });
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
fn(form).then((data) => {
|
||||
this.$emit('finished');
|
||||
this.$utils.toast(this.$t('globals.messages.created', { name: data.name }));
|
||||
this.$parent.close();
|
||||
|
@ -200,18 +209,24 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
updateRole() {
|
||||
const lists = this.form.lists.reduce((acc, item) => {
|
||||
acc.push({ id: item.id, permissions: item.permissions });
|
||||
return acc;
|
||||
}, []);
|
||||
let fn;
|
||||
const form = { id: this.$props.data.id, name: this.form.name };
|
||||
|
||||
const form = {
|
||||
id: this.$props.data.id, name: this.form.name, permissions: this.form.permissions, lists,
|
||||
};
|
||||
this.$api.updateRole(form).then((data) => {
|
||||
if (this.$props.type === 'user') {
|
||||
fn = this.$api.updateUserRole;
|
||||
form.permissions = this.form.permissions;
|
||||
} else {
|
||||
fn = this.$api.updateListRole;
|
||||
form.lists = this.form.lists.reduce((acc, item) => {
|
||||
acc.push({ id: item.id, permissions: item.permissions });
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
fn(form).then((data) => {
|
||||
this.$emit('finished');
|
||||
this.$parent.close();
|
||||
this.$utils.toast(this.$t('globals.messages.updated', { name: data.name }));
|
||||
this.$parent.close();
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -221,7 +236,7 @@ export default Vue.extend({
|
|||
|
||||
// Return the list of unselected lists.
|
||||
filteredLists() {
|
||||
if (!this.lists.results) {
|
||||
if (!this.lists.results || this.type !== 'list') {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
|
@ -3,22 +3,22 @@
|
|||
<header class="columns page-header">
|
||||
<div class="column is-10">
|
||||
<h1 class="title is-4">
|
||||
{{ $t('users.roles') }}
|
||||
{{ $t(isUser ? 'users.userRoles' : 'users.listRoles') }}
|
||||
<span v-if="!isNaN(roles.length)">({{ roles.length }})</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
<b-field v-if="$can('users:manage')" expanded>
|
||||
<b-button expanded type="is-primary" icon-left="plus" class="btn-new" @click="showNewForm" data-cy="btn-new">
|
||||
<b-button expanded type="is-primary" icon-left="plus" class="btn-new" @click="showNewForm('user')"
|
||||
data-cy="btn-new">
|
||||
{{ $t('globals.buttons.new') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<b-table :data="roles" :loading="loading.roles" hoverable>
|
||||
<b-table :data="roles" :loading="isLoading()" hoverable>
|
||||
<b-table-column v-slot="props" field="role" :label="$tc('users.role')" sortable>
|
||||
<a href="#" @click.prevent="showEditForm(props.row)">
|
||||
<a href="#" @click.prevent="showEditForm(props.row, 'user')">
|
||||
<b-tag v-if="props.row.id === 1" class="enabled">
|
||||
{{ props.row.name }}
|
||||
</b-tag>
|
||||
|
@ -36,7 +36,7 @@
|
|||
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" cell-class="actions" align="right">
|
||||
<b-table-column v-slot="props" cell-class="actions has-text-right">
|
||||
<template v-if="$can('roles:manage')">
|
||||
<a href="#" @click.prevent="$utils.prompt($t('globals.buttons.clone'),
|
||||
{
|
||||
|
@ -50,7 +50,7 @@
|
|||
</a>
|
||||
|
||||
<template v-if="props.row.id !== 1">
|
||||
<a href="#" @click.prevent="showEditForm(props.row)" data-cy="btn-edit"
|
||||
<a href="#" @click.prevent="showEditForm(props.row, 'user')" data-cy="btn-edit"
|
||||
:aria-label="$t('globals.buttons.edit')">
|
||||
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
||||
<b-icon icon="pencil-outline" size="is-small" />
|
||||
|
@ -67,14 +67,14 @@
|
|||
</template>
|
||||
</b-table-column>
|
||||
|
||||
<template #empty v-if="!loading.users">
|
||||
<template #empty v-if="!isLoading()">
|
||||
<empty-placeholder />
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<!-- Add / edit form modal -->
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="700" @close="onFormClose">
|
||||
<role-form :data="curItem" :is-editing="isEditing" @finished="formFinished" />
|
||||
<role-form :data="curItem" :type="curType" :is-editing="isEditing" @finished="formFinished" />
|
||||
</b-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
@ -94,28 +94,41 @@ export default Vue.extend({
|
|||
data() {
|
||||
return {
|
||||
curItem: null,
|
||||
curType: null,
|
||||
isEditing: false,
|
||||
isFormVisible: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
isLoading() {
|
||||
return this.curType === 'user' ? this.loading.userRoles : this.loading.listRoles;
|
||||
},
|
||||
|
||||
fetchRoles() {
|
||||
if (this.isUser) {
|
||||
this.$api.getUserRoles();
|
||||
} else {
|
||||
this.$api.getListRoles();
|
||||
}
|
||||
},
|
||||
|
||||
// Show the edit form.
|
||||
showEditForm(item) {
|
||||
this.curItem = item;
|
||||
this.curType = this.isUser ? 'user' : 'list';
|
||||
this.isFormVisible = true;
|
||||
this.isEditing = true;
|
||||
},
|
||||
|
||||
// Show the new form.
|
||||
showNewForm() {
|
||||
this.curItem = {};
|
||||
this.isFormVisible = true;
|
||||
this.isEditing = false;
|
||||
this.isFormVisible = true;
|
||||
},
|
||||
|
||||
formFinished() {
|
||||
this.$api.getRoles();
|
||||
this.fetchRoles();
|
||||
},
|
||||
|
||||
onFormClose() {
|
||||
|
@ -125,8 +138,18 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
onCloneRole(name, item) {
|
||||
this.$api.createRole({ name, permissions: item.permissions, lists: item.lists }).then(() => {
|
||||
this.$api.getRoles();
|
||||
const form = { name };
|
||||
let fn;
|
||||
if (this.isUser) {
|
||||
fn = this.$api.createUserRole;
|
||||
form.permissions = item.permissions;
|
||||
} else {
|
||||
fn = this.$api.createListRole;
|
||||
form.lists = item.lists;
|
||||
}
|
||||
|
||||
fn(form).then(() => {
|
||||
this.fetchRoles();
|
||||
this.$utils.toast(this.$t('globals.messages.created', { name }));
|
||||
});
|
||||
},
|
||||
|
@ -136,7 +159,7 @@ export default Vue.extend({
|
|||
this.$t('globals.messages.confirm'),
|
||||
() => {
|
||||
this.$api.deleteRole(item.id).then(() => {
|
||||
this.$api.getRoles();
|
||||
this.fetchRoles();
|
||||
|
||||
this.$utils.toast(this.$t('globals.messages.deleted', { name: item.name }));
|
||||
});
|
||||
|
@ -147,11 +170,24 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['loading', 'roles']),
|
||||
...mapState(['loading', 'userRoles', 'listRoles']),
|
||||
|
||||
isUser() {
|
||||
return this.curType === 'user';
|
||||
},
|
||||
|
||||
isList() {
|
||||
return this.curType === 'list';
|
||||
},
|
||||
|
||||
roles() {
|
||||
return this.isUser ? this.userRoles : this.listRoles;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$api.getRoles();
|
||||
this.curType = this.$route.name === 'userRoles' ? 'user' : 'list';
|
||||
this.fetchRoles();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -13,11 +13,6 @@
|
|||
</h4>
|
||||
</header>
|
||||
<section expanded class="modal-card-body">
|
||||
<b-field :label="$t('users.username')" label-position="on-border">
|
||||
<b-input :maxlength="200" v-model="form.username" name="username" ref="focus" autofocus
|
||||
:placeholder="$t('users.username')" required :message="$t('users.usernameHelp')" autocomplete="off"
|
||||
pattern="[a-zA-Z0-9_\-\.]+$" />
|
||||
</b-field>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<b-field :label="$t('users.type')" label-position="on-border">
|
||||
|
@ -45,12 +40,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<b-field :label="$tc('users.role')" label-position="on-border">
|
||||
<b-select v-model="form.roleId" name="role" required expanded>
|
||||
<option v-for="r in roles" :value="r.id" :key="r.id">
|
||||
{{ r.name }}
|
||||
</option>
|
||||
</b-select>
|
||||
<b-field :label="$t('users.username')" label-position="on-border">
|
||||
<b-input :maxlength="200" v-model="form.username" name="username" ref="focus" autofocus
|
||||
:placeholder="$t('users.username')" required :message="$t('users.usernameHelp')" autocomplete="off"
|
||||
pattern="[a-zA-Z0-9_\-\.]+$" />
|
||||
</b-field>
|
||||
|
||||
<b-field v-if="form.type !== 'api'" :label="$t('subscribers.email')" label-position="on-border">
|
||||
|
@ -62,28 +55,57 @@
|
|||
</b-field>
|
||||
|
||||
<template v-if="form.type !== 'api'">
|
||||
<b-field>
|
||||
<b-checkbox v-model="form.passwordLogin" :native-value="true">
|
||||
{{ $t('users.passwordEnable') }}
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
<div class="box">
|
||||
<b-field>
|
||||
<b-checkbox v-model="form.passwordLogin" :native-value="true">
|
||||
{{ $t('users.passwordEnable') }}
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<b-field :label="$t('users.password')" label-position="on-border">
|
||||
<b-input :disabled="!form.passwordLogin" minlength="8" :maxlength="200" v-model="form.password"
|
||||
type="password" name="password" :placeholder="$t('users.password')"
|
||||
:required="form.passwordLogin && !isEditing" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<b-field :label="$t('users.passwordRepeat')" label-position="on-border">
|
||||
<b-input :disabled="!form.passwordLogin" minlength="8" :maxlength="200" v-model="form.password2"
|
||||
type="password" name="password2" :required="form.passwordLogin && !isEditing && form.password" />
|
||||
</b-field>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<b-field :label="$t('users.password')" label-position="on-border">
|
||||
<b-input :disabled="!form.passwordLogin" minlength="8" :maxlength="200" v-model="form.password"
|
||||
type="password" name="password" :placeholder="$t('users.password')"
|
||||
:required="form.passwordLogin && !isEditing" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<b-field :label="$t('users.passwordRepeat')" label-position="on-border">
|
||||
<b-input :disabled="!form.passwordLogin" minlength="8" :maxlength="200" v-model="form.password2"
|
||||
type="password" name="password2" :required="form.passwordLogin && !isEditing && form.password" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<h5>{{ $tc('users.roles') }}</h5>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<b-field :label="$tc('users.userRole')" label-position="on-border">
|
||||
<b-select v-model="form.userRoleId" name="role" required expanded>
|
||||
<option v-for="r in userRoles" :value="r.id" :key="r.id">
|
||||
{{ r.name }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
|
||||
<div class="column is-6">
|
||||
<b-field :label="$tc('users.listRole', 0)" label-position="on-border">
|
||||
<b-select v-model="form.listRoleId" name="role" expanded>
|
||||
<option value="">— {{ $t("globals.terms.none") }} —</option>
|
||||
<option v-for="r in listRoles" :value="r.id" :key="r.id">
|
||||
{{ r.name }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="apiToken" class="user-api-token">
|
||||
<p>{{ $t('users.apiOneTimeToken') }}</p>
|
||||
<copy-text :text="apiToken" />
|
||||
|
@ -161,7 +183,9 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
createUser() {
|
||||
const form = { ...this.form, password_login: this.form.passwordLogin, role_id: this.form.roleId };
|
||||
const form = {
|
||||
...this.form, password_login: this.form.passwordLogin, user_role_id: this.form.userRoleId, list_role_id: this.form.listRoleId || null,
|
||||
};
|
||||
this.$api.createUser(form).then((data) => {
|
||||
this.$emit('finished');
|
||||
this.$utils.toast(this.$t('globals.messages.created', { name: data.name }));
|
||||
|
@ -178,7 +202,9 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
updateUser() {
|
||||
const form = { ...this.form, password_login: this.form.passwordLogin, role_id: this.form.roleId };
|
||||
const form = {
|
||||
...this.form, password_login: this.form.passwordLogin, user_role_id: this.form.userRoleId, list_role_id: this.form.listRoleId || null,
|
||||
};
|
||||
this.$api.updateUser({ id: this.data.id, ...form }).then((data) => {
|
||||
this.$emit('finished');
|
||||
this.$parent.close();
|
||||
|
@ -194,16 +220,19 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['loading', 'roles']),
|
||||
...mapState(['loading', 'userRoles', 'listRoles']),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.form = { ...this.form, ...this.$props.data };
|
||||
if (this.$props.data.role) {
|
||||
this.form.roleId = this.$props.data.role.id;
|
||||
if (this.$props.data.userRole) {
|
||||
this.form.userRoleId = this.$props.data.userRole.id;
|
||||
}
|
||||
|
||||
this.$api.getRoles();
|
||||
this.form.listRoleId = this.$props.data.listRole ? this.$props.data.listRole.id : '';
|
||||
|
||||
this.$api.getUserRoles();
|
||||
this.$api.getListRoles();
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.focus.focus();
|
||||
|
|
|
@ -47,18 +47,27 @@
|
|||
</b-tag>
|
||||
<div class="has-text-grey is-size-7">
|
||||
{{ props.row.name }}
|
||||
<b-tag v-if="props.row.type === 'api'" class="is-small api">
|
||||
<b-icon icon="code" />
|
||||
{{ $t(`users.type.${props.row.type}`) }}
|
||||
</b-tag>
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="status" :label="$tc('users.role')" header-class="cy-status" sortable
|
||||
:td-attrs="$utils.tdID">
|
||||
<b-tag :class="props.row.role.id === 1 ? 'enabled' : ''">
|
||||
{{ props.row.role.name }}
|
||||
</b-tag>
|
||||
<b-tag v-if="props.row.type === 'api'" class="primary">
|
||||
<b-icon icon="code" />
|
||||
{{ $t(`users.type.${props.row.type}`) }}
|
||||
</b-tag>
|
||||
<router-link :to="{ name: 'userRoles' }">
|
||||
<b-tag :class="props.row.userRole.id === 1 ? 'enabled' : 'primary'">
|
||||
<b-icon icon="account-outline" />
|
||||
{{ props.row.userRole.name }}
|
||||
</b-tag>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'listRoles' }">
|
||||
<b-tag v-if="props.row.listRole">
|
||||
<b-icon icon="newspaper-variant-outline" />
|
||||
{{ props.row.listRole.name }}
|
||||
</b-tag>
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="name" :label="$t('subscribers.email')" header-class="cy-name" sortable
|
||||
|
|
|
@ -602,7 +602,12 @@
|
|||
"users.login": "Login",
|
||||
"users.role": "Role | Roles",
|
||||
"users.roles": "Roles",
|
||||
"users.newRole": "New role",
|
||||
"users.userRole": "User role | User roles",
|
||||
"users.userRoles": "User roles",
|
||||
"users.listRole": "List roles | List role",
|
||||
"users.listRoles": "List roles",
|
||||
"users.newUserRole": "New user role",
|
||||
"users.newListRole": "New list role",
|
||||
"users.listPerms": "List permissions",
|
||||
"users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
|
||||
"users.perms": "Permissions",
|
||||
|
|
|
@ -267,7 +267,7 @@ func (o *Auth) Perm(next echo.HandlerFunc, perms ...string) echo.HandlerFunc {
|
|||
}
|
||||
|
||||
// If the current user is a Super Admin user, do no checks.
|
||||
if u.Role.ID == SuperAdminRoleID {
|
||||
if u.UserRole.ID == SuperAdminRoleID {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,20 @@ import (
|
|||
// GetRoles retrieves all roles.
|
||||
func (c *Core) GetRoles() ([]models.Role, error) {
|
||||
out := []models.Role{}
|
||||
if err := c.q.GetRoles.Select(&out); err != nil {
|
||||
if err := c.q.GetUserRoles.Select(&out); err != nil {
|
||||
return nil, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorFetching", "name", "{users.roles}", "error", pqErrMsg(err)))
|
||||
c.i18n.Ts("globals.messages.errorFetching", "name", "role", "error", pqErrMsg(err)))
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetListRoles retrieves all list roles.
|
||||
func (c *Core) GetListRoles() ([]models.ListRole, error) {
|
||||
out := []models.ListRole{}
|
||||
if err := c.q.GetListRoles.Select(&out); err != nil {
|
||||
return nil, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorFetching", "name", "role", "error", pqErrMsg(err)))
|
||||
}
|
||||
|
||||
// Unmarshall the nested list permissions, if any.
|
||||
|
@ -35,13 +46,25 @@ func (c *Core) GetRoles() ([]models.Role, error) {
|
|||
func (c *Core) CreateRole(r models.Role) (models.Role, error) {
|
||||
var out models.Role
|
||||
|
||||
if err := c.q.CreateRole.Get(&out, r.Name, pq.Array(r.Permissions)); err != nil {
|
||||
return models.Role{}, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
if err := c.q.CreateRole.Get(&out, r.Name, models.RoleTypeUser, pq.Array(r.Permissions)); err != nil {
|
||||
return out, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorCreating", "name", "{users.role}", "error", pqErrMsg(err)))
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CreateListRole creates a new list role.
|
||||
func (c *Core) CreateListRole(r models.ListRole) (models.ListRole, error) {
|
||||
var out models.ListRole
|
||||
|
||||
if err := c.q.CreateRole.Get(&out, r.Name, models.RoleTypeList, pq.Array([]string{})); err != nil {
|
||||
return out, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorCreating", "name", "{users.role}", "error", pqErrMsg(err)))
|
||||
}
|
||||
|
||||
if err := c.UpsertListPermissions(out.ID, r.Lists); err != nil {
|
||||
return models.Role{}, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
return out, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorCreating", "name", "{users.role}", "error", pqErrMsg(err)))
|
||||
}
|
||||
|
||||
|
@ -89,22 +112,38 @@ func (c *Core) DeleteListPermission(roleID, listID int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateRole updates a given role.
|
||||
func (c *Core) UpdateRole(id int, r models.Role) (models.Role, error) {
|
||||
// UpdateUserRole updates a given role.
|
||||
func (c *Core) UpdateUserRole(id int, r models.Role) (models.Role, error) {
|
||||
var out models.Role
|
||||
|
||||
if err := c.q.UpdateRole.Get(&out, id, r.Name, pq.Array(r.Permissions)); err != nil {
|
||||
return models.Role{}, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorUpdating", "name", "{users.role}", "error", pqErrMsg(err)))
|
||||
return out, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorUpdating", "name", "{users.userRole}", "error", pqErrMsg(err)))
|
||||
}
|
||||
|
||||
if out.ID == 0 {
|
||||
return models.Role{}, echo.NewHTTPError(http.StatusBadRequest, c.i18n.Ts("globals.messages.notFound", "name", "{users.role}"))
|
||||
return out, echo.NewHTTPError(http.StatusBadRequest, c.i18n.Ts("globals.messages.notFound", "name", "{users.userRole}"))
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UpdateListRole updates a given role.
|
||||
func (c *Core) UpdateListRole(id int, r models.ListRole) (models.ListRole, error) {
|
||||
var out models.ListRole
|
||||
|
||||
if err := c.q.UpdateRole.Get(&out, id, r.Name, pq.Array([]string{})); err != nil {
|
||||
return out, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorUpdating", "name", "{users.listRole}", "error", pqErrMsg(err)))
|
||||
}
|
||||
|
||||
if out.ID == 0 {
|
||||
return out, echo.NewHTTPError(http.StatusBadRequest, c.i18n.Ts("globals.messages.notFound", "name", "{users.listRole}"))
|
||||
}
|
||||
|
||||
if err := c.UpsertListPermissions(out.ID, r.Lists); err != nil {
|
||||
return models.Role{}, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorCreating", "name", "{users.role}", "error", pqErrMsg(err)))
|
||||
return out, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorCreating", "name", "{users.listRole}", "error", pqErrMsg(err)))
|
||||
}
|
||||
|
||||
return out, nil
|
||||
|
|
|
@ -49,7 +49,7 @@ func (c *Core) CreateUser(u models.User) (models.User, error) {
|
|||
u.Password = null.String{String: tk, Valid: true}
|
||||
}
|
||||
|
||||
if err := c.q.CreateUser.Get(&id, u.Username, u.PasswordLogin, u.Password, u.Email, u.Name, u.Type, u.RoleID, u.Status); err != nil {
|
||||
if err := c.q.CreateUser.Get(&id, u.Username, u.PasswordLogin, u.Password, u.Email, u.Name, u.Type, u.UserRoleID, u.ListRoleID, u.Status); err != nil {
|
||||
return models.User{}, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
|
||||
}
|
||||
|
@ -66,7 +66,14 @@ func (c *Core) CreateUser(u models.User) (models.User, error) {
|
|||
|
||||
// UpdateUser updates a given user.
|
||||
func (c *Core) UpdateUser(id int, u models.User) (models.User, error) {
|
||||
res, err := c.q.UpdateUser.Exec(id, u.Username, u.PasswordLogin, u.Password, u.Email, u.Name, u.Type, u.RoleID, u.Status)
|
||||
listRoleID := 0
|
||||
if u.ListRoleID == nil {
|
||||
listRoleID = -1
|
||||
} else {
|
||||
listRoleID = *u.ListRoleID
|
||||
}
|
||||
|
||||
res, err := c.q.UpdateUser.Exec(id, u.Username, u.PasswordLogin, u.Password, u.Email, u.Name, u.Type, u.UserRoleID, listRoleID, u.Status)
|
||||
if err != nil {
|
||||
return models.User{}, echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
|
||||
|
@ -149,6 +156,8 @@ func (c *Core) getUsers(id int, username, email string) ([]models.User, error) {
|
|||
}
|
||||
|
||||
for n, u := range out {
|
||||
u := u
|
||||
|
||||
if u.Password.String != "" {
|
||||
u.HasPassword = true
|
||||
u.PasswordLogin = true
|
||||
|
@ -158,39 +167,42 @@ func (c *Core) getUsers(id int, username, email string) ([]models.User, error) {
|
|||
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
|
||||
u.UserRole.ID = u.UserRoleID
|
||||
u.UserRole.Name = u.UserRoleName
|
||||
u.UserRole.Permissions = u.UserRolePerms
|
||||
u.UserRoleID = 0
|
||||
|
||||
// Prepare lookup maps.
|
||||
u.ListPermissionsMap = make(map[int]map[string]struct{})
|
||||
u.PermissionsMap = make(map[string]struct{})
|
||||
for _, p := range u.RolePerms {
|
||||
for _, p := range u.UserRolePerms {
|
||||
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{}{}
|
||||
|
||||
// List IDs with get / manage permissions.
|
||||
if perm == "list:get" {
|
||||
u.GetListIDs = append(u.GetListIDs, p.ID)
|
||||
if u.ListRoleID != nil {
|
||||
// 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)
|
||||
}
|
||||
if perm == "list:manage" {
|
||||
u.ManageListIDs = append(u.ManageListIDs, p.ID)
|
||||
}
|
||||
|
||||
u.ListRole = &models.ListRolePermissions{ID: *u.ListRoleID, Name: u.ListRoleName.String, Lists: listPerms}
|
||||
|
||||
for _, p := range listPerms {
|
||||
u.ListPermissionsMap[p.ID] = make(map[string]struct{})
|
||||
|
||||
for _, perm := range p.Permissions {
|
||||
u.ListPermissionsMap[p.ID][perm] = struct{}{}
|
||||
|
||||
// List IDs with get / manage permissions.
|
||||
if perm == "list:get" {
|
||||
u.GetListIDs = append(u.GetListIDs, p.ID)
|
||||
}
|
||||
if perm == "list:manage" {
|
||||
u.ManageListIDs = append(u.ManageListIDs, p.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,13 @@ package migrations
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/knadh/listmonk/internal/utils"
|
||||
"github.com/knadh/stuffbin"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
@ -18,14 +21,31 @@ func V3_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger
|
|||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_type') THEN
|
||||
CREATE TYPE user_type AS ENUM ('user', 'super', 'api');
|
||||
CREATE TYPE user_type AS ENUM ('user', 'api');
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_status') THEN
|
||||
CREATE TYPE user_status AS ENUM ('enabled', 'disabled');
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role_type') THEN
|
||||
CREATE TYPE role_type AS ENUM ('user', 'list');
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
type role_type NOT NULL DEFAULT 'user',
|
||||
parent_id INTEGER NULL REFERENCES roles(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
list_id INTEGER NULL REFERENCES lists(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
permissions TEXT[] NOT NULL DEFAULT '{}',
|
||||
name TEXT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS roles_idx ON roles (parent_id, list_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS roles_name_idx ON roles (type, name) WHERE name IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
|
@ -33,28 +53,20 @@ func V3_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger
|
|||
password TEXT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
avatar TEXT NULL,
|
||||
type user_type NOT NULL DEFAULT 'user',
|
||||
user_role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
|
||||
list_role_id INTEGER NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
status user_status NOT NULL DEFAULT 'disabled',
|
||||
loggedin_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
loggedin_at TIMESTAMP WITH TIME ZONE NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
parent_id INTEGER NULL REFERENCES user_roles(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
list_id INTEGER NULL REFERENCES lists(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
permissions TEXT[] NOT NULL DEFAULT '{}',
|
||||
name TEXT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS user_roles_idx ON user_roles (parent_id, list_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS user_roles_name_idx ON user_roles (name) WHERE name IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
data jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now() NOT NULL
|
||||
data JSONB DEFAULT '{}'::jsonb NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions ON sessions (id, created_at);
|
||||
`); err != nil {
|
||||
|
@ -87,24 +99,53 @@ func V3_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger
|
|||
perms = append(perms, p)
|
||||
}
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO roles (id, name, permissions) VALUES(1, 'Super Admin', $1) ON CONFLICT DO NOTHING`, pq.Array(perms)); err != nil {
|
||||
if _, err := db.Exec(`INSERT INTO roles (type, name, permissions) VALUES('user', 'Super Admin', $1) ON CONFLICT DO NOTHING`, pq.Array(perms)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create super admin.
|
||||
var (
|
||||
user = ko.String("app.admin_username")
|
||||
password = ko.String("app.admin_password")
|
||||
user = os.Getenv("LISTMONK_ADMIN_USER")
|
||||
password = os.Getenv("LISTMONK_ADMIN_PASSWORD")
|
||||
typ = "env"
|
||||
)
|
||||
if len(user) < 2 || len(password) < 8 {
|
||||
lo.Fatal("admin_username should be min 3 chars and admin_password should be min 8 chars in the config file")
|
||||
|
||||
if user != "" {
|
||||
// If the env vars are set, use those values
|
||||
if len(user) < 2 || len(password) < 8 {
|
||||
lo.Fatal("LISTMONK_ADMIN_USER should be min 3 chars and LISTMONK_ADMIN_PASSWORD should be min 8 chars")
|
||||
}
|
||||
} else if ko.Exists("app.admin_username") {
|
||||
// Legacy admin/password are set in the config or env var. Use those.
|
||||
user = ko.String("app.admin_username")
|
||||
password = ko.String("app.admin_password")
|
||||
|
||||
if len(user) < 2 || len(password) < 8 {
|
||||
lo.Fatal("admin_username should be min 3 chars and admin_password should be min 8 chars")
|
||||
}
|
||||
typ = "legacy config"
|
||||
} else {
|
||||
// None are set. Auto-generate.
|
||||
user = "admin"
|
||||
if p, err := utils.GenerateRandomString(12); err != nil {
|
||||
lo.Fatal("error generating admin password")
|
||||
} else {
|
||||
password = p
|
||||
}
|
||||
typ = "auto-generated"
|
||||
}
|
||||
|
||||
lo.Printf("creating admin user '%s'. Credential source is '%s'", user, typ)
|
||||
|
||||
if _, err := db.Exec(`
|
||||
INSERT INTO users (username, password_login, password, email, name, type, status) VALUES($1, true, CRYPT($2, GEN_SALT('bf')), $3, $4, 'super', 'enabled') ON CONFLICT DO NOTHING;
|
||||
INSERT INTO users (username, password_login, password, email, name, type, user_role_id, status) VALUES($1, true, CRYPT($2, GEN_SALT('bf')), $3, $4, 'user', 1, 'enabled') ON CONFLICT DO NOTHING;
|
||||
`, user, password, user+"@listmonk", user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if typ == "auto-generated" {
|
||||
fmt.Printf("\n\033[31mIMPORTANT! CHANGE PASSWORD AFTER LOGGING IN\033[0m\nusername: \033[32m%s\033[0m and password: \033[32m%s\033[0m\n\n", user, password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -61,6 +61,10 @@ const (
|
|||
UserStatusEnabled = "enabled"
|
||||
UserStatusDisabled = "disabled"
|
||||
|
||||
// Role.
|
||||
RoleTypeUser = "user"
|
||||
RoleTypeList = "list"
|
||||
|
||||
// BaseTpl is the name of the base template.
|
||||
BaseTpl = "base"
|
||||
|
||||
|
@ -160,18 +164,28 @@ type User struct {
|
|||
Avatar null.String `db:"avatar" json:"avatar"`
|
||||
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"`
|
||||
// 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"`
|
||||
|
||||
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:"-"`
|
||||
// Filled post-retrieval.
|
||||
UserRole struct {
|
||||
ID int `db:"-" json:"id"`
|
||||
Name string `db:"-" json:"name"`
|
||||
Permissions []string `db:"-" json:"permissions"`
|
||||
} `db:"-" json:"user_role"`
|
||||
|
||||
ListRole *ListRolePermissions `db:"-" json:"list_role"`
|
||||
|
||||
UserRoleID int `db:"user_role_id" json:"user_role_id,omitempty"`
|
||||
UserRoleName string `db:"user_role_name" json:"-"`
|
||||
ListRoleID *int `db:"list_role_id" json:"list_role_id,omitempty"`
|
||||
ListRoleName null.String `db:"list_role_name" json:"-"`
|
||||
UserRolePerms pq.StringArray `db:"user_role_permissions" json:"-"`
|
||||
ListsPermsRaw *json.RawMessage `db:"list_role_perms" json:"-"`
|
||||
|
||||
PermissionsMap map[string]struct{} `db:"-" json:"-"`
|
||||
ListPermissionsMap map[int]map[string]struct{} `db:"-" json:"-"`
|
||||
|
@ -186,9 +200,16 @@ type ListPermission struct {
|
|||
Permissions pq.StringArray `json:"permissions"`
|
||||
}
|
||||
|
||||
type ListRolePermissions struct {
|
||||
ID int `db:"-" json:"id"`
|
||||
Name string `db:"-" json:"name"`
|
||||
Lists []ListPermission `db:"-" json:"lists"`
|
||||
}
|
||||
|
||||
type Role struct {
|
||||
Base
|
||||
|
||||
Type string `db:"type" json:"type"`
|
||||
Name null.String `db:"name" json:"name"`
|
||||
Permissions pq.StringArray `db:"permissions" json:"permissions"`
|
||||
|
||||
|
@ -198,6 +219,17 @@ type Role struct {
|
|||
Lists []ListPermission `db:"-" json:"lists"`
|
||||
}
|
||||
|
||||
type ListRole struct {
|
||||
Base
|
||||
|
||||
Name null.String `db:"name" json:"name"`
|
||||
|
||||
ListID null.Int `db:"list_id" json:"-"`
|
||||
ParentID null.Int `db:"parent_id" json:"-"`
|
||||
ListsRaw json.RawMessage `db:"list_permissions" json:"-"`
|
||||
Lists []ListPermission `db:"-" json:"lists"`
|
||||
}
|
||||
|
||||
// Subscriber represents an e-mail subscriber.
|
||||
type Subscriber struct {
|
||||
Base
|
||||
|
@ -309,6 +341,7 @@ type Campaign struct {
|
|||
// List of media (attachment) IDs obtained from the next-campaign query
|
||||
// while sending a campaign.
|
||||
MediaIDs pq.Int64Array `json:"-" db:"media_id"`
|
||||
|
||||
// Fetched bodies of the attachments.
|
||||
Attachments []Attachment `json:"-" db:"-"`
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ type Queries struct {
|
|||
UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"`
|
||||
UpsertBlocklistSubscriber *sqlx.Stmt `query:"upsert-blocklist-subscriber"`
|
||||
GetSubscriber *sqlx.Stmt `query:"get-subscriber"`
|
||||
HasSubscriberLists *sqlx.Stmt `query:"has-subscriber-list"`
|
||||
HasSubscriberLists *sqlx.Stmt `query:"has-subscriber-list"`
|
||||
GetSubscribersByEmails *sqlx.Stmt `query:"get-subscribers-by-emails"`
|
||||
GetSubscriberLists *sqlx.Stmt `query:"get-subscriber-lists"`
|
||||
GetSubscriptions *sqlx.Stmt `query:"get-subscriptions"`
|
||||
|
@ -119,7 +119,8 @@ type Queries struct {
|
|||
LoginUser *sqlx.Stmt `query:"login-user"`
|
||||
|
||||
CreateRole *sqlx.Stmt `query:"create-role"`
|
||||
GetRoles *sqlx.Stmt `query:"get-roles"`
|
||||
GetUserRoles *sqlx.Stmt `query:"get-user-roles"`
|
||||
GetListRoles *sqlx.Stmt `query:"get-list-roles"`
|
||||
UpdateRole *sqlx.Stmt `query:"update-role"`
|
||||
DeleteRole *sqlx.Stmt `query:"delete-role"`
|
||||
UpsertListPermissions *sqlx.Stmt `query:"upsert-list-permissions"`
|
||||
|
|
109
queries.sql
109
queries.sql
|
@ -1045,7 +1045,7 @@ SELECT JSON_BUILD_OBJECT('version', (SELECT VERSION()),
|
|||
'size_mb', (SELECT ROUND(pg_database_size((SELECT CURRENT_DATABASE()))/(1024^2)))) AS info;
|
||||
|
||||
-- name: create-user
|
||||
INSERT INTO users (username, password_login, password, email, name, type, role_id, status)
|
||||
INSERT INTO users (username, password_login, password, email, name, type, user_role_id, list_role_id, status)
|
||||
VALUES($1, $2, (
|
||||
CASE
|
||||
-- For user types with password_login enabled, bcrypt and store the hash of the password.
|
||||
|
@ -1056,7 +1056,7 @@ INSERT INTO users (username, password_login, password, email, name, type, role_i
|
|||
THEN $3
|
||||
ELSE NULL
|
||||
END
|
||||
), $4, $5, $6, $7, $8) RETURNING id;
|
||||
), $4, $5, $6, (SELECT id FROM roles WHERE id = $7 AND type = 'user'), (SELECT id FROM roles WHERE id = $8 AND type = 'list'), $9) RETURNING id;
|
||||
|
||||
-- name: update-user
|
||||
WITH u AS (
|
||||
|
@ -1064,7 +1064,7 @@ WITH u AS (
|
|||
-- if the only superadmin user's status/role isn't being changed.
|
||||
SELECT
|
||||
CASE
|
||||
WHEN (SELECT COUNT(*) FROM users WHERE id != $1 AND status = 'enabled' AND type = 'user' AND role_id = 1) = 0 AND ($8 != 1 OR $9 != 'enabled')
|
||||
WHEN (SELECT COUNT(*) FROM users WHERE id != $1 AND status = 'enabled' AND type = 'user' AND user_role_id = 1) = 0 AND ($8 != 1 OR $10 != 'enabled')
|
||||
THEN FALSE
|
||||
ELSE TRUE
|
||||
END AS canEdit
|
||||
|
@ -1076,19 +1076,58 @@ UPDATE users SET
|
|||
email=(CASE WHEN $5 != '' THEN $5 ELSE email END),
|
||||
name=(CASE WHEN $6 != '' THEN $6 ELSE name END),
|
||||
type=(CASE WHEN $7 != '' THEN $7::user_type ELSE type END),
|
||||
role_id=(CASE WHEN $8 != 0 THEN $8 ELSE role_id END),
|
||||
status=(CASE WHEN $9 != '' THEN $9::user_status ELSE status END)
|
||||
user_role_id=(CASE WHEN $8 != 0 THEN (SELECT id FROM roles WHERE id = $8 AND type = 'user') ELSE user_role_id END),
|
||||
list_role_id=(
|
||||
CASE
|
||||
WHEN $9 < 0 THEN NULL
|
||||
WHEN $9 > 0 THEN (SELECT id FROM roles WHERE id = $9 AND type = 'list')
|
||||
ELSE list_role_id END
|
||||
),
|
||||
status=(CASE WHEN $10 != '' THEN $10::user_status ELSE status END)
|
||||
WHERE id=$1 AND (SELECT canEdit FROM u) = TRUE;
|
||||
|
||||
-- name: delete-users
|
||||
WITH u AS (
|
||||
SELECT COUNT(*) AS num FROM users WHERE NOT(id = ANY($1)) AND role_id=1 AND status='enabled'
|
||||
SELECT COUNT(*) AS num FROM users WHERE NOT(id = ANY($1)) AND user_role_id=1 AND status='enabled'
|
||||
)
|
||||
DELETE FROM users WHERE id = ALL($1) AND (SELECT num FROM u) > 0;
|
||||
|
||||
-- name: get-users
|
||||
WITH u AS (
|
||||
SELECT * FROM users
|
||||
WITH ur AS (
|
||||
SELECT id, name, permissions FROM roles WHERE type = 'user' AND parent_id IS NULL
|
||||
),
|
||||
lr AS (
|
||||
SELECT r.id, r.name, r.permissions, r.list_id, l.name AS list_name
|
||||
FROM roles r
|
||||
LEFT JOIN lists l ON r.list_id = l.id
|
||||
WHERE r.type = 'list' AND r.parent_id IS NULL
|
||||
),
|
||||
lp AS (
|
||||
SELECT lr.id AS list_role_id,
|
||||
JSONB_AGG(
|
||||
JSONB_BUILD_OBJECT(
|
||||
'id', COALESCE(cr.list_id, lr.list_id),
|
||||
'name', COALESCE(cl.name, lr.list_name),
|
||||
'permissions', COALESCE(cr.permissions, lr.permissions)
|
||||
)
|
||||
) AS list_role_perms
|
||||
FROM lr
|
||||
LEFT JOIN roles cr ON cr.parent_id = lr.id AND cr.type = 'list'
|
||||
LEFT JOIN lists cl ON cr.list_id = cl.id
|
||||
GROUP BY lr.id
|
||||
)
|
||||
SELECT
|
||||
users.*,
|
||||
ur.id AS user_role_id,
|
||||
ur.name AS user_role_name,
|
||||
ur.permissions AS user_role_permissions,
|
||||
lp.list_role_id,
|
||||
lr.name AS list_role_name,
|
||||
lp.list_role_perms
|
||||
FROM users
|
||||
LEFT JOIN ur ON users.user_role_id = ur.id
|
||||
LEFT JOIN lp ON users.list_role_id = lp.list_role_id
|
||||
LEFT JOIN lr ON lp.list_role_id = lr.id
|
||||
WHERE
|
||||
(
|
||||
CASE
|
||||
|
@ -1097,23 +1136,7 @@ WITH u AS (
|
|||
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-api-tokens
|
||||
|
@ -1122,7 +1145,7 @@ SELECT username, password FROM users WHERE status='enabled' AND type='api';
|
|||
-- name: login-user
|
||||
WITH u AS (
|
||||
SELECT users.*, r.name as role_name, r.permissions FROM users
|
||||
LEFT JOIN user_roles r ON (r.id = users.role_id)
|
||||
LEFT JOIN roles r ON (r.id = users.user_role_id)
|
||||
WHERE username=$1 AND status != 'disabled' AND password_login = TRUE
|
||||
)
|
||||
SELECT * FROM u WHERE CRYPT($2, password) = password;
|
||||
|
@ -1135,40 +1158,54 @@ UPDATE users SET name=$2, email=(CASE WHEN password_login THEN $3 ELSE email END
|
|||
-- name: update-user-login
|
||||
UPDATE users SET loggedin_at=NOW(), avatar=(CASE WHEN $2 != '' THEN $2 ELSE avatar END) WHERE id=$1;
|
||||
|
||||
-- name: get-roles
|
||||
-- name: get-user-roles
|
||||
WITH mainroles AS (
|
||||
SELECT ur.* FROM user_roles ur WHERE ur.parent_id IS NULL
|
||||
SELECT ur.* FROM roles ur WHERE type = 'user' AND ur.parent_id IS NULL
|
||||
),
|
||||
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
|
||||
FROM 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 listPerms l ON p.id = l.parent_id ORDER BY p.created_at;
|
||||
|
||||
-- name: get-list-roles
|
||||
WITH mainroles AS (
|
||||
SELECT ur.* FROM roles ur WHERE type = 'list' AND ur.parent_id IS NULL
|
||||
),
|
||||
listPerms AS (
|
||||
SELECT ur.parent_id, JSONB_AGG(JSONB_BUILD_OBJECT('id', ur.list_id, 'name', lists.name, 'permissions', ur.permissions)) AS listPerms
|
||||
FROM 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 listPerms l ON p.id = l.parent_id ORDER BY p.created_at;
|
||||
|
||||
|
||||
-- name: create-role
|
||||
INSERT INTO user_roles (name, permissions, created_at, updated_at) VALUES($1, $2, NOW(), NOW()) RETURNING *;
|
||||
INSERT INTO roles (name, type, permissions, created_at, updated_at) VALUES($1, $2, $3, NOW(), NOW()) RETURNING *;
|
||||
|
||||
-- name: upsert-list-permissions
|
||||
WITH d AS (
|
||||
-- Delete lists that aren't included.
|
||||
DELETE FROM user_roles WHERE parent_id = $1 AND list_id != ALL($2::INT[])
|
||||
DELETE FROM roles WHERE parent_id = $1 AND list_id != ALL($2::INT[])
|
||||
),
|
||||
p AS (
|
||||
-- Get (list_id, perms[]), (list_id, perms[])
|
||||
SELECT UNNEST($2) AS list_id, JSONB_ARRAY_ELEMENTS(TO_JSONB($3::TEXT[][])) AS perms
|
||||
)
|
||||
INSERT INTO user_roles (parent_id, list_id, permissions)
|
||||
SELECT $1, list_id, ARRAY_REMOVE(ARRAY(SELECT JSONB_ARRAY_ELEMENTS_TEXT(perms)), '') FROM p
|
||||
INSERT INTO roles (parent_id, list_id, permissions, type)
|
||||
SELECT $1, list_id, ARRAY_REMOVE(ARRAY(SELECT JSONB_ARRAY_ELEMENTS_TEXT(perms)), ''), 'list' FROM p
|
||||
ON CONFLICT (parent_id, list_id) DO UPDATE SET permissions = EXCLUDED.permissions;
|
||||
|
||||
-- name: delete-list-permission
|
||||
DELETE FROM user_roles WHERE parent_id=$1 AND list_id=$2;
|
||||
DELETE FROM roles WHERE parent_id=$1 AND list_id=$2;
|
||||
|
||||
-- name: update-role
|
||||
UPDATE user_roles SET name=$2, permissions=$3 WHERE id=$1 and parent_id IS NULL RETURNING *;
|
||||
UPDATE roles SET name=$2, permissions=$3 WHERE id=$1 and parent_id IS NULL RETURNING *;
|
||||
|
||||
-- name: delete-role
|
||||
DELETE FROM user_roles WHERE id=$1;
|
||||
DELETE FROM roles WHERE id=$1;
|
||||
|
|
19
schema.sql
19
schema.sql
|
@ -9,6 +9,7 @@ DROP TYPE IF EXISTS bounce_type CASCADE; CREATE TYPE bounce_type AS ENUM ('soft'
|
|||
DROP TYPE IF EXISTS template_type CASCADE; CREATE TYPE template_type AS ENUM ('campaign', 'tx');
|
||||
DROP TYPE IF EXISTS user_type CASCADE; CREATE TYPE user_type AS ENUM ('user', 'api');
|
||||
DROP TYPE IF EXISTS user_status CASCADE; CREATE TYPE user_status AS ENUM ('enabled', 'disabled');
|
||||
DROP TYPE IF EXISTS role_type CASCADE; CREATE TYPE role_type AS ENUM ('user', 'list');
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
|
@ -301,18 +302,19 @@ DROP INDEX IF EXISTS idx_bounces_source; CREATE INDEX idx_bounces_source ON boun
|
|||
DROP INDEX IF EXISTS idx_bounces_date; CREATE INDEX idx_bounces_date ON bounces((TIMEZONE('UTC', created_at)::DATE));
|
||||
|
||||
-- roles
|
||||
DROP TABLE IF EXISTS user_roles CASCADE;
|
||||
CREATE TABLE user_roles (
|
||||
DROP TABLE IF EXISTS roles CASCADE;
|
||||
CREATE TABLE roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
parent_id INTEGER NULL REFERENCES user_roles(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
type role_type NOT NULL DEFAULT 'user',
|
||||
parent_id INTEGER NULL REFERENCES roles(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
list_id INTEGER NULL REFERENCES lists(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
permissions TEXT[] NOT NULL DEFAULT '{}',
|
||||
name TEXT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
CREATE UNIQUE INDEX user_roles_idx ON user_roles (parent_id, list_id);
|
||||
CREATE UNIQUE INDEX user_roles_name_idx ON user_roles (name) WHERE name IS NOT NULL;
|
||||
CREATE UNIQUE INDEX roles_idx ON roles (parent_id, list_id);
|
||||
CREATE UNIQUE INDEX roles_name_idx ON roles (type, name) WHERE name IS NOT NULL;
|
||||
|
||||
-- users
|
||||
DROP TABLE IF EXISTS users CASCADE;
|
||||
|
@ -325,7 +327,8 @@ CREATE TABLE users (
|
|||
name TEXT NOT NULL,
|
||||
avatar TEXT NULL,
|
||||
type user_type NOT NULL DEFAULT 'user',
|
||||
role_id INTEGER NOT NULL REFERENCES user_roles(id) ON DELETE RESTRICT,
|
||||
user_role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
|
||||
list_role_id INTEGER NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
status user_status NOT NULL DEFAULT 'disabled',
|
||||
loggedin_at TIMESTAMP WITH TIME ZONE NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
@ -336,8 +339,8 @@ CREATE TABLE users (
|
|||
DROP TABLE IF EXISTS sessions CASCADE;
|
||||
CREATE TABLE sessions (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
data jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now() NOT NULL
|
||||
data JSONB DEFAULT '{}'::jsonb NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL
|
||||
);
|
||||
DROP INDEX IF EXISTS idx_sessions; CREATE INDEX idx_sessions ON sessions (id, created_at);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue