nuxt-users 1.48.2 → 1.49.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,11 +4,10 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/nuxt-users.svg)](https://www.npmjs.com/package/nuxt-users)
5
5
  [![License](https://img.shields.io/npm/l/nuxt-users.svg)](https://github.com/rrd108/nuxt-users/blob/main/LICENSE)
6
6
  [![ci](https://github.com/rrd108/nuxt-users/actions/workflows/ci.yml/badge.svg)](https://github.com/rrd108/nuxt-users/actions/workflows/ci.yml)
7
- [![Nuxt 3](https://img.shields.io/badge/Nuxt-3-00DC82.svg?logo=nuxt.js&logoColor=white)](https://nuxt.com)
8
- [![Nuxt 4](https://img.shields.io/badge/Nuxt-4-00DC82.svg?logo=nuxt.js&logoColor=white)](https://nuxt.com)
7
+ [![Nuxt](https://img.shields.io/badge/Nuxt-00DC82.svg?logo=nuxt.js&logoColor=white)](https://nuxt.com)
9
8
 
10
9
 
11
- A user authentication module for Nuxt 3 and Nuxt 4 with database support for SQLite, MySQL, and PostgreSQL.
10
+ A user authentication module for Nuxt with database support for SQLite, MySQL, and PostgreSQL.
12
11
 
13
12
  ## Features
14
13
 
@@ -36,7 +35,7 @@ A user authentication module for Nuxt 3 and Nuxt 4 with database support for SQL
36
35
  - ⚡ **Developer Experience**
37
36
  - Zero-config setup with sensible defaults
38
37
  - TypeScript support throughout
39
- - Nuxt 3 & 4 compatibility
38
+ - Nuxt compatibility
40
39
  - Easy customization and extension
41
40
 
42
41
  ## Agent Skill
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from 'citty';
3
3
  import { readFileSync } from 'node:fs';
4
- import { d as defaultOptions } from './shared/nuxt-users.BsNZbqYb.mjs';
4
+ import { d as defaultOptions } from './shared/nuxt-users.KrHIaTTL.mjs';
5
5
  import { defu } from 'defu';
6
6
  import { runMigrations, getAppliedMigrations } from '../dist/runtime/server/utils/migrate.js';
7
7
  import { createUser as createUser$1 } from '../dist/runtime/server/utils/user.js';
package/dist/module.d.mts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { ModuleOptions } from './utils.mjs';
2
+ import { ModuleOptions, RuntimeModuleOptions } from './utils.mjs';
3
3
  export * from '../dist/runtime/server/internal/index.js';
4
4
 
5
5
  declare const defaultOptions: ModuleOptions;
6
6
 
7
- declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
7
+ declare const _default: _nuxt_schema.NuxtModule<RuntimeModuleOptions, RuntimeModuleOptions, false>;
8
8
 
9
9
  export { _default as default, defaultOptions };
package/dist/module.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { ModuleOptions } from './utils.js';
2
+ import { ModuleOptions, RuntimeModuleOptions } from './utils.js';
3
3
  export * from '../dist/runtime/server/internal/index.js';
4
4
 
5
5
  declare const defaultOptions: ModuleOptions;
6
6
 
7
- declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
7
+ declare const _default: _nuxt_schema.NuxtModule<RuntimeModuleOptions, RuntimeModuleOptions, false>;
8
8
 
9
9
  export { _default as default, defaultOptions };
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "1.48.2",
7
+ "version": "1.49.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { defineNuxtModule, createResolver, addPlugin, addImportsDir, addServerImportsDir, addServerScanDir, addServerHandler, addServerPlugin, addRouteMiddleware, addComponentsDir } from '@nuxt/kit';
2
2
  import { defu } from 'defu';
3
- import { d as defaultOptions } from './shared/nuxt-users.BsNZbqYb.mjs';
3
+ import { d as defaultOptions } from './shared/nuxt-users.KrHIaTTL.mjs';
4
4
  export * from '../dist/runtime/server/internal/index.js';
5
5
 
6
6
  const module$1 = defineNuxtModule({
@@ -22,12 +22,14 @@ const module$1 = defineNuxtModule({
22
22
  }
23
23
  const configuredConnector = nuxt.options.runtimeConfig.nuxtUsers?.connector || options.connector;
24
24
  runtimeConfigOptions.connector = configuredConnector || defaultOptions.connector;
25
+ runtimeConfigOptions.auth.allowCustomRoles = runtimeConfigOptions.auth.allowCustomRoles ?? defaultOptions.auth.allowCustomRoles;
25
26
  nuxt.options.runtimeConfig.nuxtUsers = {
26
27
  ...runtimeConfigOptions,
27
28
  auth: {
28
29
  tokenExpiration: runtimeConfigOptions.auth.tokenExpiration,
29
30
  rememberMeExpiration: runtimeConfigOptions.auth.rememberMeExpiration,
30
31
  permissions: runtimeConfigOptions.auth.permissions,
32
+ allowCustomRoles: runtimeConfigOptions.auth.allowCustomRoles,
31
33
  ...runtimeConfigOptions.auth.google && { google: runtimeConfigOptions.auth.google },
32
34
  whitelist: (() => {
33
35
  const combinedWhitelist = [...defaultOptions.auth?.whitelist || [], ...runtimeConfigOptions.auth?.whitelist || []];
@@ -99,7 +101,8 @@ const module$1 = defineNuxtModule({
99
101
  }
100
102
  return combinedWhitelist;
101
103
  })(),
102
- permissions: runtimeConfigOptions.auth?.permissions || defaultOptions.auth.permissions
104
+ permissions: runtimeConfigOptions.auth?.permissions || defaultOptions.auth.permissions,
105
+ allowCustomRoles: runtimeConfigOptions.auth.allowCustomRoles
103
106
  },
104
107
  apiBasePath: runtimeConfigOptions.apiBasePath || defaultOptions.apiBasePath
105
108
  };
@@ -1,7 +1,10 @@
1
1
  <script setup>
2
2
  import { ref, computed, watch } from "vue";
3
+ import { getAvailableRoles, MAX_ROLE_LENGTH } from "nuxt-users/utils";
3
4
  import { usePasswordValidation } from "../composables/usePasswordValidation";
5
+ import { useNuxtUsersLocale } from "../composables/useNuxtUsersLocale";
4
6
  import { useRuntimeConfig } from "#imports";
7
+ const CUSTOM_ROLE_OPTION = "__custom__";
5
8
  const props = defineProps({
6
9
  user: { type: null, required: false }
7
10
  });
@@ -12,19 +15,55 @@ const emptyFormData = {
12
15
  role: "user",
13
16
  password: ""
14
17
  };
15
- const formData = ref(props.user ?? emptyFormData);
18
+ const formData = ref(props.user ?? { ...emptyFormData });
16
19
  const { public: { nuxtUsers } } = useRuntimeConfig();
17
20
  const moduleOptions = nuxtUsers;
18
21
  const passwordValidation = usePasswordValidation(moduleOptions);
22
+ const { t } = useNuxtUsersLocale();
19
23
  const nameError = ref("");
24
+ const roleError = ref("");
25
+ const roleSelectValue = ref("");
20
26
  const isEditMode = computed(() => !!props.user);
21
27
  const title = computed(() => isEditMode.value ? "Edit User" : "Create User");
22
28
  const submitText = computed(() => isEditMode.value ? "Update" : "Create");
29
+ const availableRoles = computed(() => getAvailableRoles(moduleOptions.auth?.permissions));
30
+ const allowCustomRoles = computed(() => moduleOptions.auth?.allowCustomRoles === true);
31
+ const useRoleSelect = computed(() => availableRoles.value.length > 0);
32
+ const roleOptions = computed(() => {
33
+ const roles = [...availableRoles.value];
34
+ const currentRole = formData.value.role;
35
+ if (currentRole && !roles.includes(currentRole) && currentRole !== CUSTOM_ROLE_OPTION) {
36
+ roles.push(currentRole);
37
+ }
38
+ return roles;
39
+ });
40
+ const isCustomRoleSelected = computed(() => roleSelectValue.value === CUSTOM_ROLE_OPTION);
41
+ const syncRoleSelectFromForm = () => {
42
+ if (!useRoleSelect.value) {
43
+ roleSelectValue.value = "";
44
+ return;
45
+ }
46
+ const currentRole = formData.value.role || "";
47
+ if (roleOptions.value.includes(currentRole)) {
48
+ roleSelectValue.value = currentRole;
49
+ return;
50
+ }
51
+ if (allowCustomRoles.value) {
52
+ roleSelectValue.value = CUSTOM_ROLE_OPTION;
53
+ return;
54
+ }
55
+ roleSelectValue.value = roleOptions.value[0] || "user";
56
+ formData.value.role = roleSelectValue.value;
57
+ };
23
58
  watch(() => props.user, (newUser) => {
24
59
  if (newUser) {
25
60
  formData.value = { ...newUser };
26
61
  }
27
- });
62
+ if (!newUser) {
63
+ formData.value = { ...emptyFormData };
64
+ }
65
+ syncRoleSelectFromForm();
66
+ }, { immediate: true });
28
67
  watch(() => formData.value.name, (name) => {
29
68
  nameError.value = "";
30
69
  if (!name || name.trim().length < 3) {
@@ -39,12 +78,55 @@ watch(() => formData.value.password, (newPassword) => {
39
78
  passwordValidation.clearValidation();
40
79
  }
41
80
  });
81
+ watch(roleSelectValue, (value) => {
82
+ roleError.value = "";
83
+ if (!useRoleSelect.value) {
84
+ return;
85
+ }
86
+ if (value === CUSTOM_ROLE_OPTION) {
87
+ if (!allowCustomRoles.value) {
88
+ return;
89
+ }
90
+ if (roleOptions.value.includes(formData.value.role)) {
91
+ formData.value.role = "";
92
+ }
93
+ return;
94
+ }
95
+ formData.value.role = value;
96
+ });
97
+ const validateRole = () => {
98
+ roleError.value = "";
99
+ const role = (formData.value.role || "").trim();
100
+ if (!role) {
101
+ roleError.value = "Role is required";
102
+ return false;
103
+ }
104
+ if (role.length > MAX_ROLE_LENGTH) {
105
+ roleError.value = `Role must be at most ${MAX_ROLE_LENGTH} characters`;
106
+ return false;
107
+ }
108
+ if (!useRoleSelect.value) {
109
+ return true;
110
+ }
111
+ if (allowCustomRoles.value && isCustomRoleSelected.value) {
112
+ return true;
113
+ }
114
+ if (roleOptions.value.includes(role)) {
115
+ return true;
116
+ }
117
+ roleError.value = `Invalid role. Allowed roles: ${availableRoles.value.join(", ")}`;
118
+ return false;
119
+ };
42
120
  const handleSubmit = async () => {
43
121
  const userData = { ...formData.value };
44
122
  if (!userData.name || userData.name.trim().length < 3) {
45
123
  console.log("name is too short");
46
124
  return;
47
125
  }
126
+ if (!validateRole()) {
127
+ return;
128
+ }
129
+ userData.role = (userData.role || "").trim();
48
130
  if (isEditMode.value && !userData.password) {
49
131
  delete userData.password;
50
132
  }
@@ -67,6 +149,7 @@ const handleSubmit = async () => {
67
149
  }
68
150
  emit("submit", userData);
69
151
  formData.value = { ...emptyFormData };
152
+ syncRoleSelectFromForm();
70
153
  } catch (error) {
71
154
  console.error("Failed to save user:", error);
72
155
  emit("error", error);
@@ -82,6 +165,7 @@ const handleSubmit = async () => {
82
165
  <div class="n-users-form-group">
83
166
  <label for="name">Name:</label>
84
167
  <input
168
+ id="name"
85
169
  v-model="formData.name"
86
170
  name="name"
87
171
  type="text"
@@ -100,6 +184,7 @@ const handleSubmit = async () => {
100
184
  <div class="n-users-form-group">
101
185
  <label for="email">Email:</label>
102
186
  <input
187
+ id="email"
103
188
  v-model="formData.email"
104
189
  type="email"
105
190
  required
@@ -108,10 +193,60 @@ const handleSubmit = async () => {
108
193
 
109
194
  <div class="n-users-form-group">
110
195
  <label for="role">Role:</label>
196
+
197
+ <template v-if="useRoleSelect">
198
+ <select
199
+ id="role"
200
+ v-model="roleSelectValue"
201
+ name="role"
202
+ required
203
+ :class="{ error: roleError }"
204
+ >
205
+ <option
206
+ v-for="role in roleOptions"
207
+ :key="role"
208
+ :value="role"
209
+ >
210
+ {{ role }}
211
+ </option>
212
+ <option
213
+ v-if="allowCustomRoles"
214
+ :value="CUSTOM_ROLE_OPTION"
215
+ >
216
+ {{ t("userForm.customRoleOption") }}
217
+ </option>
218
+ </select>
219
+
220
+ <input
221
+ v-if="allowCustomRoles && isCustomRoleSelected"
222
+ id="custom-role"
223
+ v-model="formData.role"
224
+ type="text"
225
+ name="custom-role"
226
+ :maxlength="MAX_ROLE_LENGTH"
227
+ required
228
+ :placeholder="t('userForm.customRolePlaceholder')"
229
+ :class="{ error: roleError }"
230
+ >
231
+ </template>
232
+
111
233
  <input
234
+ v-else
235
+ id="role"
112
236
  v-model="formData.role"
113
237
  type="text"
238
+ name="role"
239
+ :maxlength="MAX_ROLE_LENGTH"
240
+ required
241
+ :class="{ error: roleError }"
114
242
  >
243
+
244
+ <small
245
+ v-if="roleError"
246
+ class="n-users-error-text"
247
+ >
248
+ {{ roleError }}
249
+ </small>
115
250
  </div>
116
251
 
117
252
  <div class="n-users-form-group">
@@ -1,8 +1,8 @@
1
1
  import { useState, useRuntimeConfig } from "#imports";
2
- const usersState = useState("users", () => []);
3
- const paginationState = useState("users-pagination", () => null);
4
2
  export const useUsers = () => {
5
3
  const { public: { nuxtUsers } } = useRuntimeConfig();
4
+ const usersState = useState("users", () => []);
5
+ const paginationState = useState("users-pagination", () => null);
6
6
  const loading = useState("users-loading", () => false);
7
7
  const error = useState("users-error", () => null);
8
8
  const fetchUsers = async (page = 1, limit = 100) => {
@@ -148,5 +148,9 @@ export const en = {
148
148
  users: "users",
149
149
  previous: "Previous",
150
150
  next: "Next"
151
+ },
152
+ userForm: {
153
+ customRoleOption: "Custom\u2026",
154
+ customRolePlaceholder: "Enter custom role"
151
155
  }
152
156
  };
@@ -130,6 +130,9 @@ const huBase = {
130
130
  users: "felhaszn\xE1l\xF3",
131
131
  previous: "El\u0151z\u0151",
132
132
  next: "K\xF6vetkez\u0151"
133
+ },
134
+ userForm: {
135
+ customRoleOption: "Egyedi\u2026"
133
136
  }
134
137
  };
135
138
  const huInformalOverrides = {
@@ -167,6 +170,9 @@ const huInformalOverrides = {
167
170
  processingMessage: "K\xE9rj\xFCk, v\xE1rj, am\xEDg feldolgozzuk az e-mail meger\u0151s\xEDt\xE9s\xE9t.",
168
171
  successMessage: "Az e-mail c\xEDmed meger\u0151s\xEDt\xE9sre ker\xFClt \xE9s a fi\xF3kod akt\xEDv.",
169
172
  errorMessage: "A meger\u0151s\xEDt\u0151 link \xE9rv\xE9nytelen vagy lej\xE1rt. K\xE9rj\xFCk, pr\xF3b\xE1ld \xFAjra regisztr\xE1lni vagy l\xE9pj kapcsolatba az \xFCgyf\xE9lszolg\xE1lattal."
173
+ },
174
+ userForm: {
175
+ customRolePlaceholder: "Add meg az egyedi szerepk\xF6rt"
170
176
  }
171
177
  };
172
178
  const huFormalOverrides = {
@@ -204,6 +210,9 @@ const huFormalOverrides = {
204
210
  processingMessage: "K\xE9rj\xFCk, v\xE1rjon, am\xEDg feldolgozzuk az e-mail meger\u0151s\xEDt\xE9s\xE9t.",
205
211
  successMessage: "Az e-mail c\xEDme meger\u0151s\xEDt\xE9sre ker\xFClt \xE9s a fi\xF3kja akt\xEDv.",
206
212
  errorMessage: "A meger\u0151s\xEDt\u0151 link \xE9rv\xE9nytelen vagy lej\xE1rt. K\xE9rj\xFCk, pr\xF3b\xE1ljon meg \xFAjra regisztr\xE1lni vagy l\xE9pjen kapcsolatba az \xFCgyf\xE9lszolg\xE1lattal."
213
+ },
214
+ userForm: {
215
+ customRolePlaceholder: "Adja meg az egyedi szerepk\xF6rt"
207
216
  }
208
217
  };
209
218
  export const hu = deepMerge(huBase, huInformalOverrides);
@@ -23,9 +23,11 @@ export default defineEventHandler(async (event) => {
23
23
  const updatedUser = await updateUser(userId, body, options);
24
24
  return { user: updatedUser };
25
25
  } catch (error) {
26
+ const message = error?.message || "Unknown error";
27
+ const isClientError = typeof message === "string" && (message.startsWith("Invalid role") || message.startsWith("Role ") || message === "User not found.");
26
28
  throw createError({
27
- statusCode: 500,
28
- statusMessage: `Error updating user: ${error.message}`
29
+ statusCode: isClientError ? message === "User not found." ? 404 : 400 : 500,
30
+ statusMessage: `Error updating user: ${message}`
29
31
  });
30
32
  }
31
33
  });
@@ -15,13 +15,16 @@ export default defineEventHandler(async (event) => {
15
15
  const newUser = await createUser({
16
16
  email: body.email,
17
17
  name: body.name,
18
- password: body.password
18
+ password: body.password,
19
+ role: body.role
19
20
  }, options);
20
21
  return { user: newUser };
21
22
  } catch (error) {
23
+ const message = error?.message || "Unknown error";
24
+ const isClientError = typeof message === "string" && (message.startsWith("Invalid role") || message.startsWith("Role ") || message.startsWith("Password validation"));
22
25
  throw createError({
23
- statusCode: 500,
24
- statusMessage: `Error creating user: ${error.message}`
26
+ statusCode: isClientError ? 400 : 500,
27
+ statusMessage: `Error creating user: ${message}`
25
28
  });
26
29
  }
27
30
  });
@@ -26,7 +26,7 @@ export const getConnector = async (name) => {
26
26
  throw new Error(`Database connector "${name}" not found. Please install the required peer dependency:
27
27
  - For sqlite: yarn add better-sqlite3
28
28
  - For mysql: yarn add mysql2
29
- - For postgresql: yarn add pg`);
29
+ - For postgresql: yarn add pg`, { cause: error });
30
30
  }
31
31
  throw error;
32
32
  }
@@ -1,6 +1,6 @@
1
1
  import { useDb } from "./db.js";
2
2
  import bcrypt from "bcrypt";
3
- import { validatePassword, getPasswordValidationOptions } from "nuxt-users/utils";
3
+ import { validatePassword, getPasswordValidationOptions, assertValidRole } from "nuxt-users/utils";
4
4
  export const createUser = async (userData, options) => {
5
5
  const db = await useDb(options);
6
6
  const usersTable = options.tables.users;
@@ -10,7 +10,10 @@ export const createUser = async (userData, options) => {
10
10
  throw new Error(`Password validation failed: ${passwordValidation.errors.join(", ")}`);
11
11
  }
12
12
  const hashedPassword = await bcrypt.hash(userData.password, 10);
13
- const role = userData.role || "user";
13
+ const role = assertValidRole(userData.role || "user", {
14
+ permissions: options.auth.permissions,
15
+ allowCustomRoles: options.auth.allowCustomRoles
16
+ });
14
17
  await db.sql`
15
18
  INSERT INTO {${usersTable}} (email, name, password, role, created_at, updated_at)
16
19
  VALUES (${userData.email}, ${userData.name}, ${hashedPassword}, ${role}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
@@ -89,6 +92,12 @@ export const updateUser = async (id, userData, options) => {
89
92
  }
90
93
  return currentUser;
91
94
  }
95
+ if (userData.role !== void 0) {
96
+ userData.role = assertValidRole(userData.role, {
97
+ permissions: options.auth.permissions,
98
+ allowCustomRoles: options.auth.allowCustomRoles
99
+ });
100
+ }
92
101
  if (userData.active === false) {
93
102
  await revokeUserTokens(id, options);
94
103
  }
@@ -140,6 +140,13 @@ interface RuntimeModuleOptions {
140
140
  * }
141
141
  */
142
142
  permissions?: Record<string, (string | Permission)[]>;
143
+ /**
144
+ * Allow assigning roles that are not defined in auth.permissions
145
+ * When false (default), create/update user role must be a key of permissions
146
+ * (or any role if permissions is empty / not yet configured)
147
+ * @default false
148
+ */
149
+ allowCustomRoles?: boolean;
143
150
  /**
144
151
  * Google OAuth configuration
145
152
  * Enable Google OAuth login/registration
@@ -225,6 +232,11 @@ interface ModuleOptions {
225
232
  tokenExpiration: number;
226
233
  rememberMeExpiration: number;
227
234
  permissions: Record<string, (string | Permission)[]>;
235
+ /**
236
+ * Allow assigning roles that are not defined in auth.permissions
237
+ * @default false
238
+ */
239
+ allowCustomRoles: boolean;
228
240
  google?: GoogleOAuthOptions;
229
241
  };
230
242
  passwordValidation: {
@@ -337,4 +349,4 @@ declare const defaultFieldLabels: {
337
349
  };
338
350
 
339
351
  export { defaultDisplayFields as j, defaultFieldLabels as k };
340
- export type { DatabaseConfig as D, GoogleOAuthOptions as G, LocaleMessages as L, ModuleOptions as M, PasswordResetToken as P, ResetPasswordFormProps as R, User as U, DatabaseType as a, DisplayFieldsProps as b, LocaleOptions as c, LoginFormData as d, LoginFormProps as e, Permission as f, PersonalAccessToken as g, RuntimeModuleOptions as h, UserWithoutPassword as i };
352
+ export type { DatabaseConfig as D, GoogleOAuthOptions as G, LocaleMessages as L, ModuleOptions as M, Permission as P, RuntimeModuleOptions as R, User as U, DatabaseType as a, DisplayFieldsProps as b, LocaleOptions as c, LoginFormData as d, LoginFormProps as e, PasswordResetToken as f, PersonalAccessToken as g, ResetPasswordFormProps as h, UserWithoutPassword as i };
@@ -140,6 +140,13 @@ interface RuntimeModuleOptions {
140
140
  * }
141
141
  */
142
142
  permissions?: Record<string, (string | Permission)[]>;
143
+ /**
144
+ * Allow assigning roles that are not defined in auth.permissions
145
+ * When false (default), create/update user role must be a key of permissions
146
+ * (or any role if permissions is empty / not yet configured)
147
+ * @default false
148
+ */
149
+ allowCustomRoles?: boolean;
143
150
  /**
144
151
  * Google OAuth configuration
145
152
  * Enable Google OAuth login/registration
@@ -225,6 +232,11 @@ interface ModuleOptions {
225
232
  tokenExpiration: number;
226
233
  rememberMeExpiration: number;
227
234
  permissions: Record<string, (string | Permission)[]>;
235
+ /**
236
+ * Allow assigning roles that are not defined in auth.permissions
237
+ * @default false
238
+ */
239
+ allowCustomRoles: boolean;
228
240
  google?: GoogleOAuthOptions;
229
241
  };
230
242
  passwordValidation: {
@@ -337,4 +349,4 @@ declare const defaultFieldLabels: {
337
349
  };
338
350
 
339
351
  export { defaultDisplayFields as j, defaultFieldLabels as k };
340
- export type { DatabaseConfig as D, GoogleOAuthOptions as G, LocaleMessages as L, ModuleOptions as M, PasswordResetToken as P, ResetPasswordFormProps as R, User as U, DatabaseType as a, DisplayFieldsProps as b, LocaleOptions as c, LoginFormData as d, LoginFormProps as e, Permission as f, PersonalAccessToken as g, RuntimeModuleOptions as h, UserWithoutPassword as i };
352
+ export type { DatabaseConfig as D, GoogleOAuthOptions as G, LocaleMessages as L, ModuleOptions as M, Permission as P, RuntimeModuleOptions as R, User as U, DatabaseType as a, DisplayFieldsProps as b, LocaleOptions as c, LoginFormData as d, LoginFormProps as e, PasswordResetToken as f, PersonalAccessToken as g, ResetPasswordFormProps as h, UserWithoutPassword as i };
@@ -30,7 +30,8 @@ const defaultOptions = {
30
30
  whitelist: [],
31
31
  tokenExpiration: 24 * 60,
32
32
  rememberMeExpiration: 30,
33
- permissions: {}
33
+ permissions: {},
34
+ allowCustomRoles: false
34
35
  },
35
36
  passwordValidation: {
36
37
  minLength: 8,
package/dist/utils.d.mts CHANGED
@@ -1,7 +1,8 @@
1
+ import { P as Permission } from './shared/nuxt-users.DtwPhils.mjs';
2
+ export { D as DatabaseConfig, a as DatabaseType, b as DisplayFieldsProps, G as GoogleOAuthOptions, L as LocaleMessages, c as LocaleOptions, d as LoginFormData, e as LoginFormProps, M as ModuleOptions, f as PasswordResetToken, g as PersonalAccessToken, h as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, i as UserWithoutPassword, j as defaultDisplayFields, k as defaultFieldLabels } from './shared/nuxt-users.DtwPhils.mjs';
1
3
  export { defaultLocaleMessages, en, hu, huFormal } from '../dist/runtime/locales/index.js';
2
4
  export { getNestedValue, getTranslation } from '../dist/runtime/utils/locale.js';
3
5
  export { deepMerge } from '../dist/runtime/utils/deep-merge.js';
4
- export { D as DatabaseConfig, a as DatabaseType, b as DisplayFieldsProps, G as GoogleOAuthOptions, L as LocaleMessages, c as LocaleOptions, d as LoginFormData, e as LoginFormProps, M as ModuleOptions, P as PasswordResetToken, f as Permission, g as PersonalAccessToken, R as ResetPasswordFormProps, h as RuntimeModuleOptions, U as User, i as UserWithoutPassword, j as defaultDisplayFields, k as defaultFieldLabels } from './shared/nuxt-users.bLYb3Bz5.mjs';
5
6
 
6
7
  interface PasswordValidationResult {
7
8
  isValid: boolean;
@@ -28,5 +29,27 @@ declare const validatePassword: (password: string, options?: PasswordValidationO
28
29
  declare const getPasswordStrengthColor: (strength: "weak" | "medium" | "strong") => string;
29
30
  declare const getPasswordStrengthText: (strength: "weak" | "medium" | "strong") => string;
30
31
 
31
- export { getPasswordStrengthColor, getPasswordStrengthText, getPasswordValidationOptions, validatePassword };
32
- export type { PasswordValidationOptions, PasswordValidationResult };
32
+ declare const MAX_ROLE_LENGTH = 32;
33
+ type RoleValidationOptions = {
34
+ permissions?: Record<string, (string | Permission)[]>;
35
+ allowCustomRoles?: boolean;
36
+ };
37
+ /**
38
+ * Returns role names defined in auth.permissions (the predefined role catalog).
39
+ */
40
+ declare const getAvailableRoles: (permissions?: Record<string, (string | Permission)[]>) => string[];
41
+ /**
42
+ * Validates a role against permissions and allowCustomRoles.
43
+ * - Empty permissions: any non-empty role is allowed (bootstrap before RBAC is configured)
44
+ * - allowCustomRoles true: any non-empty role is allowed
45
+ * - otherwise: role must be a key of permissions
46
+ */
47
+ declare const isValidRole: (role: string | undefined | null, options?: RoleValidationOptions) => boolean;
48
+ /**
49
+ * Asserts role is valid or throws with a clear error message.
50
+ * Returns the trimmed role string.
51
+ */
52
+ declare const assertValidRole: (role: string | undefined | null, options?: RoleValidationOptions) => string;
53
+
54
+ export { MAX_ROLE_LENGTH, Permission, assertValidRole, getAvailableRoles, getPasswordStrengthColor, getPasswordStrengthText, getPasswordValidationOptions, isValidRole, validatePassword };
55
+ export type { PasswordValidationOptions, PasswordValidationResult, RoleValidationOptions };
package/dist/utils.d.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import { P as Permission } from './shared/nuxt-users.DtwPhils.js';
2
+ export { D as DatabaseConfig, a as DatabaseType, b as DisplayFieldsProps, G as GoogleOAuthOptions, L as LocaleMessages, c as LocaleOptions, d as LoginFormData, e as LoginFormProps, M as ModuleOptions, f as PasswordResetToken, g as PersonalAccessToken, h as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, i as UserWithoutPassword, j as defaultDisplayFields, k as defaultFieldLabels } from './shared/nuxt-users.DtwPhils.js';
1
3
  export { defaultLocaleMessages, en, hu, huFormal } from '../dist/runtime/locales/index.js';
2
4
  export { getNestedValue, getTranslation } from '../dist/runtime/utils/locale.js';
3
5
  export { deepMerge } from '../dist/runtime/utils/deep-merge.js';
4
- export { D as DatabaseConfig, a as DatabaseType, b as DisplayFieldsProps, G as GoogleOAuthOptions, L as LocaleMessages, c as LocaleOptions, d as LoginFormData, e as LoginFormProps, M as ModuleOptions, P as PasswordResetToken, f as Permission, g as PersonalAccessToken, R as ResetPasswordFormProps, h as RuntimeModuleOptions, U as User, i as UserWithoutPassword, j as defaultDisplayFields, k as defaultFieldLabels } from './shared/nuxt-users.bLYb3Bz5.js';
5
6
 
6
7
  interface PasswordValidationResult {
7
8
  isValid: boolean;
@@ -28,5 +29,27 @@ declare const validatePassword: (password: string, options?: PasswordValidationO
28
29
  declare const getPasswordStrengthColor: (strength: "weak" | "medium" | "strong") => string;
29
30
  declare const getPasswordStrengthText: (strength: "weak" | "medium" | "strong") => string;
30
31
 
31
- export { getPasswordStrengthColor, getPasswordStrengthText, getPasswordValidationOptions, validatePassword };
32
- export type { PasswordValidationOptions, PasswordValidationResult };
32
+ declare const MAX_ROLE_LENGTH = 32;
33
+ type RoleValidationOptions = {
34
+ permissions?: Record<string, (string | Permission)[]>;
35
+ allowCustomRoles?: boolean;
36
+ };
37
+ /**
38
+ * Returns role names defined in auth.permissions (the predefined role catalog).
39
+ */
40
+ declare const getAvailableRoles: (permissions?: Record<string, (string | Permission)[]>) => string[];
41
+ /**
42
+ * Validates a role against permissions and allowCustomRoles.
43
+ * - Empty permissions: any non-empty role is allowed (bootstrap before RBAC is configured)
44
+ * - allowCustomRoles true: any non-empty role is allowed
45
+ * - otherwise: role must be a key of permissions
46
+ */
47
+ declare const isValidRole: (role: string | undefined | null, options?: RoleValidationOptions) => boolean;
48
+ /**
49
+ * Asserts role is valid or throws with a clear error message.
50
+ * Returns the trimmed role string.
51
+ */
52
+ declare const assertValidRole: (role: string | undefined | null, options?: RoleValidationOptions) => string;
53
+
54
+ export { MAX_ROLE_LENGTH, Permission, assertValidRole, getAvailableRoles, getPasswordStrengthColor, getPasswordStrengthText, getPasswordValidationOptions, isValidRole, validatePassword };
55
+ export type { PasswordValidationOptions, PasswordValidationResult, RoleValidationOptions };
package/dist/utils.mjs CHANGED
@@ -3,6 +3,53 @@ export { defaultLocaleMessages, en, hu, huFormal } from '../dist/runtime/locales
3
3
  export { getNestedValue, getTranslation } from '../dist/runtime/utils/locale.js';
4
4
  export { deepMerge } from '../dist/runtime/utils/deep-merge.js';
5
5
 
6
+ const MAX_ROLE_LENGTH = 32;
7
+ const getAvailableRoles = (permissions) => {
8
+ if (!permissions) {
9
+ return [];
10
+ }
11
+ return Object.keys(permissions);
12
+ };
13
+ const isValidRole = (role, options = {}) => {
14
+ if (role === void 0 || role === null) {
15
+ return false;
16
+ }
17
+ const trimmed = typeof role === "string" ? role.trim() : "";
18
+ if (!trimmed) {
19
+ return false;
20
+ }
21
+ if (trimmed.length > MAX_ROLE_LENGTH) {
22
+ return false;
23
+ }
24
+ if (options.allowCustomRoles) {
25
+ return true;
26
+ }
27
+ const availableRoles = getAvailableRoles(options.permissions);
28
+ if (availableRoles.length === 0) {
29
+ return true;
30
+ }
31
+ return availableRoles.includes(trimmed);
32
+ };
33
+ const assertValidRole = (role, options = {}) => {
34
+ if (role === void 0 || role === null || typeof role !== "string") {
35
+ throw new Error("Role is required");
36
+ }
37
+ const trimmed = role.trim();
38
+ if (!trimmed) {
39
+ throw new Error("Role is required");
40
+ }
41
+ if (trimmed.length > MAX_ROLE_LENGTH) {
42
+ throw new Error(`Role must be at most ${MAX_ROLE_LENGTH} characters`);
43
+ }
44
+ if (isValidRole(trimmed, options)) {
45
+ return trimmed;
46
+ }
47
+ const availableRoles = getAvailableRoles(options.permissions);
48
+ throw new Error(
49
+ `Invalid role "${trimmed}". Allowed roles: ${availableRoles.join(", ")}`
50
+ );
51
+ };
52
+
6
53
  const defaultDisplayFields = ["id", "name", "email", "role", "active", "created_at", "updated_at", "last_login_at"];
7
54
  const defaultFieldLabels = {
8
55
  id: "ID",
@@ -15,4 +62,4 @@ const defaultFieldLabels = {
15
62
  last_login_at: "Last Login"
16
63
  };
17
64
 
18
- export { defaultDisplayFields, defaultFieldLabels };
65
+ export { MAX_ROLE_LENGTH, assertValidRole, defaultDisplayFields, defaultFieldLabels, getAvailableRoles, isValidRole };
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "nuxt-users",
3
- "version": "1.48.2",
4
- "description": "A comprehensive user management module for Nuxt 3 and Nuxt 4 applications with authentication, authorization, database support, and CLI tools",
3
+ "version": "1.49.1",
4
+ "description": "A comprehensive user management module for Nuxt applications with authentication, authorization, database support, and CLI tools",
5
5
  "keywords": [
6
6
  "nuxt",
7
- "nuxt3",
8
7
  "nuxt-module",
9
8
  "users",
10
9
  "authentication",
@@ -83,7 +82,7 @@
83
82
  "test:postgresql": "./scripts/test-postgresql.sh",
84
83
  "test:nuxt3": "./scripts/test-nuxt3.sh unit && ./scripts/test-nuxt3.sh integration && ./scripts/test-nuxt3.sh sqlite && ./scripts/test-nuxt3.sh mysql && ./scripts/test-nuxt3.sh postgresql",
85
84
  "test:nuxt4": "yarn test:types && yarn test:unit && yarn test:integration && yarn test:sqlite && yarn test:mysql && yarn test:postgresql",
86
- "test": "yarn lint && yarn test:nuxt3 && yarn test:nuxt4",
85
+ "test": "yarn lint && yarn test:nuxt4",
87
86
  "test:watch": "vitest watch",
88
87
  "db:create-users-table": "tsx src/cli/create-users-table.ts",
89
88
  "db:create-user": "tsx src/cli/create-user.ts",
@@ -100,7 +99,7 @@
100
99
  "dependencies": {
101
100
  "bcrypt": "^6.0.0",
102
101
  "citty": "^0.2.0",
103
- "db0": "^0.3.4",
102
+ "db0": "^0.4.0",
104
103
  "defu": "^6.1.4",
105
104
  "google-auth-library": "^10.4.0",
106
105
  "nodemailer": "^7.0.5"
@@ -144,5 +143,8 @@
144
143
  "vitepress": "^1.6.3",
145
144
  "vitest": "^4.0.0",
146
145
  "vue-tsc": "^3.0.1"
146
+ },
147
+ "resolutions": {
148
+ "entities": "^7.0.0"
147
149
  }
148
150
  }