nuxt-users 1.34.7 → 1.35.0

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cli.mjs +74 -0
  3. package/dist/module.d.mts +1 -1
  4. package/dist/module.d.ts +1 -1
  5. package/dist/module.json +1 -1
  6. package/dist/module.mjs +38 -3
  7. package/dist/runtime/assets/nuxt-users.css +1 -1
  8. package/dist/runtime/components/NUsersGoogleLoginButton.vue +75 -0
  9. package/dist/runtime/components/NUsersGoogleLoginButton.vue.d.ts +30 -0
  10. package/dist/runtime/composables/useAuthentication.d.ts +1 -1
  11. package/dist/runtime/composables/useAuthentication.js +26 -8
  12. package/dist/runtime/middleware/authorization.client.js +9 -7
  13. package/dist/runtime/server/api/nuxt-users/auth/google/callback.get.d.ts +2 -0
  14. package/dist/runtime/server/api/nuxt-users/auth/google/callback.get.js +68 -0
  15. package/dist/runtime/server/api/nuxt-users/auth/google/redirect.get.d.ts +2 -0
  16. package/dist/runtime/server/api/nuxt-users/auth/google/redirect.get.js +39 -0
  17. package/dist/runtime/server/utils/add-google-oauth-fields.d.ts +2 -0
  18. package/dist/runtime/server/utils/add-google-oauth-fields.js +22 -0
  19. package/dist/runtime/server/utils/create-users-table.js +7 -1
  20. package/dist/runtime/server/utils/google-oauth.d.ts +33 -0
  21. package/dist/runtime/server/utils/google-oauth.js +136 -0
  22. package/dist/runtime/server/utils/index.d.ts +1 -0
  23. package/dist/runtime/server/utils/index.js +1 -0
  24. package/dist/runtime/server/utils/migrate.js +5 -0
  25. package/dist/shared/{nuxt-users.BTVH9EyH.d.mts → nuxt-users.SXaMnaEL.d.mts} +45 -1
  26. package/dist/shared/{nuxt-users.BTVH9EyH.d.ts → nuxt-users.SXaMnaEL.d.ts} +45 -1
  27. package/dist/utils.d.mts +1 -1
  28. package/dist/utils.d.ts +1 -1
  29. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,51 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## v1.35.0
5
+
6
+ [compare changes](https://github.com/rrd108/nuxt-users/compare/v1.34.7...v1.35.0)
7
+
8
+ ### 🚀 Enhancements
9
+
10
+ - Implement google authentication ([e830872](https://github.com/rrd108/nuxt-users/commit/e830872))
11
+ - Add google auth to the playground ([2b43ca0](https://github.com/rrd108/nuxt-users/commit/2b43ca0))
12
+ - Add migration ([0280eca](https://github.com/rrd108/nuxt-users/commit/0280eca))
13
+ - Unregistered user gets error message ([eefbdf5](https://github.com/rrd108/nuxt-users/commit/eefbdf5))
14
+
15
+ ### 🩹 Fixes
16
+
17
+ - Prevents database connection issues during build ([8a3aa38](https://github.com/rrd108/nuxt-users/commit/8a3aa38))
18
+ - Add api endpoints to public endpoints ([16aefa9](https://github.com/rrd108/nuxt-users/commit/16aefa9))
19
+ - Completes Google OAuth user fetch on client ([6c95b94](https://github.com/rrd108/nuxt-users/commit/6c95b94))
20
+
21
+ ### 📖 Documentation
22
+
23
+ - Fix github link ([28662ca](https://github.com/rrd108/nuxt-users/commit/28662ca))
24
+ - Adds Google OAuth authentication ([463c19f](https://github.com/rrd108/nuxt-users/commit/463c19f))
25
+ - Adds Google OAuth registration ([2e0eebf](https://github.com/rrd108/nuxt-users/commit/2e0eebf))
26
+
27
+ ### 🏡 Chore
28
+
29
+ - Lint fix ([8d75930](https://github.com/rrd108/nuxt-users/commit/8d75930))
30
+ - **release:** V1.34.7 ([b9d061c](https://github.com/rrd108/nuxt-users/commit/b9d061c))
31
+ - Remove unused files ([8bd3e58](https://github.com/rrd108/nuxt-users/commit/8bd3e58))
32
+ - Replace googleapis package with google-auth ([0e8e3d4](https://github.com/rrd108/nuxt-users/commit/0e8e3d4))
33
+ - Fix lint errors ([3ed5bcc](https://github.com/rrd108/nuxt-users/commit/3ed5bcc))
34
+ - Update lock ([8cd4a97](https://github.com/rrd108/nuxt-users/commit/8cd4a97))
35
+
36
+ ### ✅ Tests
37
+
38
+ - Google OAuth authentication ([7d7bd68](https://github.com/rrd108/nuxt-users/commit/7d7bd68))
39
+ - Add oauth flow test ([b805718](https://github.com/rrd108/nuxt-users/commit/b805718))
40
+ - Fix db setup ([a097c4a](https://github.com/rrd108/nuxt-users/commit/a097c4a))
41
+ - Replace smoke test with middleware test ([7ee1893](https://github.com/rrd108/nuxt-users/commit/7ee1893))
42
+ - Add security checks ([a206962](https://github.com/rrd108/nuxt-users/commit/a206962))
43
+ - Fix ts errors ([d7e6c61](https://github.com/rrd108/nuxt-users/commit/d7e6c61))
44
+
45
+ ### ❤️ Contributors
46
+
47
+ - Rrd108 ([@rrd108](https://github.com/rrd108))
48
+
4
49
  ## v1.34.7
5
50
 
6
51
  [compare changes](https://github.com/rrd108/nuxt-users/compare/v1.34.6...v1.34.7)
package/dist/cli.mjs CHANGED
@@ -7,6 +7,7 @@ import { defu } from 'defu';
7
7
  import { runMigrations, createUser as createUser$1, createUsersTable as createUsersTable$1, createPersonalAccessTokensTable as createPersonalAccessTokensTable$1, createPasswordResetTokensTable as createPasswordResetTokensTable$1, createMigrationsTable as createMigrationsTable$1 } from '../dist/runtime/server/utils/index.js';
8
8
  import { g as getPasswordValidationOptions, v as validatePassword } from './shared/nuxt-users.BnOQs2LI.mjs';
9
9
  import { addActiveToUsers as addActiveToUsers$1 } from '../dist/runtime/server/utils/add-active-to-users.js';
10
+ import { useDb } from '../dist/runtime/server/utils/db.js';
10
11
  import '../dist/runtime/server/internal/index.js';
11
12
 
12
13
  const getOptionsFromEnv = () => {
@@ -245,6 +246,78 @@ const addActiveToUsers = defineCommand({
245
246
  }
246
247
  });
247
248
 
249
+ const addGoogleOauthFields = defineCommand({
250
+ meta: {
251
+ name: "add-google-oauth-fields",
252
+ description: "Add Google OAuth fields (google_id, profile_picture) to existing users table"
253
+ },
254
+ async run() {
255
+ console.log("[Nuxt Users] Adding Google OAuth fields to users table...");
256
+ const options = await loadOptions();
257
+ const connectorName = options.connector.name;
258
+ const db = await useDb(options);
259
+ const tableName = options.tables.users;
260
+ console.log(`[Nuxt Users] DB:Migrate ${connectorName} Users Table Adding Google OAuth fields...`);
261
+ try {
262
+ if (connectorName === "sqlite") {
263
+ const tableInfo = await db.sql`PRAGMA table_info(${tableName})`;
264
+ const columnNames = tableInfo.rows.map((row) => row.name);
265
+ if (!columnNames.includes("google_id")) {
266
+ await db.sql`ALTER TABLE {${tableName}} ADD COLUMN google_id TEXT`;
267
+ console.log("[Nuxt Users] Added google_id column to SQLite users table \u2705");
268
+ }
269
+ if (!columnNames.includes("profile_picture")) {
270
+ await db.sql`ALTER TABLE {${tableName}} ADD COLUMN profile_picture TEXT`;
271
+ console.log("[Nuxt Users] Added profile_picture column to SQLite users table \u2705");
272
+ }
273
+ if (!columnNames.includes("last_login_at")) {
274
+ await db.sql`ALTER TABLE {${tableName}} ADD COLUMN last_login_at DATETIME`;
275
+ console.log("[Nuxt Users] Added last_login_at column to SQLite users table \u2705");
276
+ }
277
+ }
278
+ if (connectorName === "mysql") {
279
+ const checkColumns = await db.sql`
280
+ SELECT COLUMN_NAME
281
+ FROM INFORMATION_SCHEMA.COLUMNS
282
+ WHERE TABLE_SCHEMA = DATABASE()
283
+ AND TABLE_NAME = ${tableName}
284
+ AND COLUMN_NAME IN ('google_id', 'profile_picture')
285
+ `;
286
+ const existingColumns = checkColumns.rows.map((row) => row.COLUMN_NAME);
287
+ if (!existingColumns.includes("google_id")) {
288
+ await db.sql`ALTER TABLE {${tableName}} ADD COLUMN google_id VARCHAR(255) UNIQUE`;
289
+ console.log("[Nuxt Users] Added google_id column to MySQL users table \u2705");
290
+ }
291
+ if (!existingColumns.includes("profile_picture")) {
292
+ await db.sql`ALTER TABLE {${tableName}} ADD COLUMN profile_picture TEXT`;
293
+ console.log("[Nuxt Users] Added profile_picture column to MySQL users table \u2705");
294
+ }
295
+ }
296
+ if (connectorName === "postgresql") {
297
+ const checkColumns = await db.sql`
298
+ SELECT column_name
299
+ FROM information_schema.columns
300
+ WHERE table_name = ${tableName}
301
+ AND column_name IN ('google_id', 'profile_picture')
302
+ `;
303
+ const existingColumns = checkColumns.rows.map((row) => row.column_name);
304
+ if (!existingColumns.includes("google_id")) {
305
+ await db.sql`ALTER TABLE {${tableName}} ADD COLUMN google_id VARCHAR(255) UNIQUE`;
306
+ console.log("[Nuxt Users] Added google_id column to PostgreSQL users table \u2705");
307
+ }
308
+ if (!existingColumns.includes("profile_picture")) {
309
+ await db.sql`ALTER TABLE {${tableName}} ADD COLUMN profile_picture TEXT`;
310
+ console.log("[Nuxt Users] Added profile_picture column to PostgreSQL users table \u2705");
311
+ }
312
+ }
313
+ console.log("[Nuxt Users] Google OAuth fields migration completed successfully! \u2705");
314
+ } catch (error) {
315
+ console.error("[Nuxt Users] DB:Migration Error:", error);
316
+ process.exit(1);
317
+ }
318
+ }
319
+ });
320
+
248
321
  const projectInfo = defineCommand({
249
322
  meta: {
250
323
  name: "project-info",
@@ -304,6 +377,7 @@ const main = defineCommand({
304
377
  "create-password-reset-tokens-table": createPasswordResetTokensTable,
305
378
  "create-migrations-table": createMigrationsTable,
306
379
  "add-active-to-users": addActiveToUsers,
380
+ "add-google-oauth-fields": addGoogleOauthFields,
307
381
  "project-info": projectInfo
308
382
  }
309
383
  });
package/dist/module.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { R as RuntimeModuleOptions, M as ModuleOptions } from './shared/nuxt-users.BTVH9EyH.mjs';
2
+ import { R as RuntimeModuleOptions, M as ModuleOptions } from './shared/nuxt-users.SXaMnaEL.mjs';
3
3
  export * from '../dist/runtime/server/internal/index.js';
4
4
 
5
5
  declare const defaultOptions: ModuleOptions;
package/dist/module.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { R as RuntimeModuleOptions, M as ModuleOptions } from './shared/nuxt-users.BTVH9EyH.js';
2
+ import { R as RuntimeModuleOptions, M as ModuleOptions } from './shared/nuxt-users.SXaMnaEL.js';
3
3
  export * from '../dist/runtime/server/internal/index.js';
4
4
 
5
5
  declare const defaultOptions: ModuleOptions;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-users",
3
3
  "configKey": "nuxtUsers",
4
- "version": "1.34.7",
4
+ "version": "1.35.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -65,11 +65,14 @@ const module = defineNuxtModule({
65
65
  nuxt.options.runtimeConfig.nuxtUsers = {
66
66
  ...runtimeConfigOptions,
67
67
  auth: {
68
- ...runtimeConfigOptions.auth,
68
+ tokenExpiration: runtimeConfigOptions.auth.tokenExpiration,
69
+ rememberMeExpiration: runtimeConfigOptions.auth.rememberMeExpiration,
70
+ permissions: runtimeConfigOptions.auth.permissions,
71
+ ...runtimeConfigOptions.auth.google && { google: runtimeConfigOptions.auth.google },
69
72
  whitelist: (() => {
70
73
  const combinedWhitelist = [...defaultOptions.auth?.whitelist || [], ...runtimeConfigOptions.auth?.whitelist || []];
74
+ const apiBasePath = runtimeConfigOptions.apiBasePath || defaultOptions.apiBasePath;
71
75
  if (combinedWhitelist.includes("/register")) {
72
- const apiBasePath = runtimeConfigOptions.apiBasePath || defaultOptions.apiBasePath;
73
76
  const registrationEndpoints = [
74
77
  "/confirm-email",
75
78
  // Page route for email confirmation
@@ -84,6 +87,17 @@ const module = defineNuxtModule({
84
87
  }
85
88
  });
86
89
  }
90
+ if (runtimeConfigOptions.auth?.google) {
91
+ const googleOAuthEndpoints = [
92
+ `${apiBasePath}/auth/google/redirect`,
93
+ `${apiBasePath}/auth/google/callback`
94
+ ];
95
+ googleOAuthEndpoints.forEach((endpoint) => {
96
+ if (!combinedWhitelist.includes(endpoint)) {
97
+ combinedWhitelist.push(endpoint);
98
+ }
99
+ });
100
+ }
87
101
  return combinedWhitelist;
88
102
  })()
89
103
  }
@@ -94,8 +108,8 @@ const module = defineNuxtModule({
94
108
  auth: {
95
109
  whitelist: (() => {
96
110
  const combinedWhitelist = [...defaultOptions.auth?.whitelist || [], ...runtimeConfigOptions.auth?.whitelist || []];
111
+ const apiBasePath = runtimeConfigOptions.apiBasePath || defaultOptions.apiBasePath;
97
112
  if (combinedWhitelist.includes("/register")) {
98
- const apiBasePath = runtimeConfigOptions.apiBasePath || defaultOptions.apiBasePath;
99
113
  const registrationEndpoints = [
100
114
  "/confirm-email",
101
115
  // Page route for email confirmation
@@ -110,6 +124,17 @@ const module = defineNuxtModule({
110
124
  }
111
125
  });
112
126
  }
127
+ if (runtimeConfigOptions.auth?.google) {
128
+ const googleOAuthEndpoints = [
129
+ `${apiBasePath}/auth/google/redirect`,
130
+ `${apiBasePath}/auth/google/callback`
131
+ ];
132
+ googleOAuthEndpoints.forEach((endpoint) => {
133
+ if (!combinedWhitelist.includes(endpoint)) {
134
+ combinedWhitelist.push(endpoint);
135
+ }
136
+ });
137
+ }
113
138
  return combinedWhitelist;
114
139
  })(),
115
140
  permissions: runtimeConfigOptions.auth?.permissions || defaultOptions.auth.permissions
@@ -177,6 +202,16 @@ const module = defineNuxtModule({
177
202
  method: "get",
178
203
  handler: resolver.resolve("./runtime/server/api/nuxt-users/confirm-email.get")
179
204
  });
205
+ addServerHandler({
206
+ route: `${base}/auth/google/redirect`,
207
+ method: "get",
208
+ handler: resolver.resolve("./runtime/server/api/nuxt-users/auth/google/redirect.get")
209
+ });
210
+ addServerHandler({
211
+ route: `${base}/auth/google/callback`,
212
+ method: "get",
213
+ handler: resolver.resolve("./runtime/server/api/nuxt-users/auth/google/callback.get")
214
+ });
180
215
  addServerHandler({
181
216
  route: `${base}`,
182
217
  method: "get",
@@ -1 +1 @@
1
- :root{--nu-color-primary:#3b82f6;--nu-color-primary-dark:#2563eb;--nu-color-primary-light:#60a5fa;--nu-color-white:#fff;--nu-color-gray-50:#f9fafb;--nu-color-gray-500:#6b7280;--nu-color-gray-600:#4b5563;--nu-color-gray-700:#374151;--nu-color-gray-900:#111827;--nu-color-success:#059669;--nu-color-success-light:#f0fdf4;--nu-color-success-border:#9ae6b4;--nu-color-error:#dc2626;--nu-color-error-light:#fef2f2;--nu-color-error-border:#fecaca;--nu-color-error-alt:#db5050;--nu-color-error-dark:#b91c1c;--nu-color-border:#e5e7eb;--nu-color-border-light:#d1d5db;--nu-color-border-dark:#ccc;--nu-color-bg-primary:#fff;--nu-color-bg-secondary:#f9fafb;--nu-color-bg-tertiary:#f3f4f6;--nu-color-text-primary:#000;--nu-color-text-secondary:#6b7280;--nu-color-strength-bg:#e9ecef;--nu-color-validation-passed:#28a745;--nu-color-validation-failed:#dc3545;--nu-color-validation-hint:#6c757d}.dark :root{--nu-color-primary:#60a5fa;--nu-color-primary-dark:#3b82f6;--nu-color-primary-light:#93c5fd;--nu-color-white:#000;--nu-color-gray-50:#111827;--nu-color-gray-500:#9ca3af;--nu-color-gray-600:#d1d5db;--nu-color-gray-700:#e5e7eb;--nu-color-gray-900:#f9fafb;--nu-color-success:#10b981;--nu-color-success-light:#064e3b;--nu-color-success-border:#065f46;--nu-color-error:#ef4444;--nu-color-error-light:#450a0a;--nu-color-error-border:#7f1d1d;--nu-color-error-alt:#f87171;--nu-color-error-dark:#dc2626;--nu-color-border:#374151;--nu-color-border-light:#4b5563;--nu-color-border-dark:#6b7280;--nu-color-bg-primary:#111827;--nu-color-bg-secondary:#1f2937;--nu-color-bg-tertiary:#374151;--nu-color-text-primary:#fff;--nu-color-text-secondary:#9ca3af;--nu-color-strength-bg:#374151;--nu-color-validation-passed:#22c55e;--nu-color-validation-failed:#ef4444;--nu-color-validation-hint:#9ca3af}.n-users-form-group{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;width:100%}.n-users-form-group.n-users-form-group-narrow{width:90%}.n-users-form-group label,.n-users-form-label{color:var(--nu-color-text-primary,#000);display:block;font-size:1rem;font-weight:500;margin-bottom:.5rem}.n-users-form-group input,.n-users-form-group select,.n-users-form-input{border:1px solid var(--nu-color-border,#ccc);border-radius:.25rem;font-size:1rem;padding:.5rem;transition:border-color .2s,box-shadow .2s;width:100%}.n-users-form-group input:focus,.n-users-form-group select:focus,.n-users-form-input:focus{border-color:var(--nu-color-primary);box-shadow:0 0 0 .1rem rgba(59,130,246,.1);outline:none}.n-users-form-group input:disabled,.n-users-form-input:disabled{background-color:var(--nu-color-bg-secondary,#f9fafb);cursor:not-allowed}.n-users-form-group input.error,.n-users-form-input.error{border-color:var(--nu-color-error,#dc3545)}.n-users-form-group input[type=email],.n-users-form-group input[type=password],.n-users-form-group input[type=text]{background-color:var(--nu-color-bg-secondary);border:1px solid var(--nu-color-border-light);border-radius:.5em;box-sizing:border-box;color:var(--nu-color-gray-700);font-size:.875rem;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}.n-users-form-group input[type=email]:focus,.n-users-form-group input[type=password]:focus,.n-users-form-group input[type=text]:focus{background-color:var(--nu-color-bg-primary);border-color:var(--nu-color-primary);box-shadow:0 0 0 3px rgba(59,130,246,.1);outline:none}.n-users-form-group input[type=checkbox]{margin:0;width:auto}.n-users-btn,.n-users-cancel-btn,.n-users-delete-btn,.n-users-edit-btn,.n-users-submit-btn{align-self:flex-start;border:none;border-radius:.25rem;cursor:pointer;display:inline-block;font-size:1rem;font-weight:500;padding:.5rem 1rem;text-decoration:none;transition:background-color .2s,transform .1s}.n-users-btn:hover,.n-users-edit-btn:hover,.n-users-submit-btn:hover{background-color:var(--nu-color-primary-dark,#2563eb);transform:translateY(-1px)}.n-users-btn:disabled,.n-users-submit-btn:disabled{cursor:not-allowed;opacity:.6;transform:none}.n-users-btn-primary,.n-users-edit-btn,.n-users-submit-btn{background-color:var(--nu-color-primary,#3b82f6);color:#fff}.n-users-btn-secondary,.n-users-cancel-btn{background-color:var(--nu-color-gray-500,#6b7280);color:#fff}.n-users-cancel-btn:hover{background-color:var(--nu-color-gray-600,#4b5563)}.n-users-delete-btn{background-color:var(--nu-color-error,#ef4444);color:#fff}.n-users-delete-btn:hover{background-color:var(--nu-color-error-alt,#dc2626)}.n-users-login-section button[type=submit],.n-users-register-section button[type=submit],.n-users-reset-password-container button[type=submit],.n-users-user-form button[type=submit],form.n-users-form button[type=submit]{align-items:center;background-color:var(--nu-color-primary);border:1px solid var(--nu-color-primary);border-radius:.5em;color:var(--nu-color-white);cursor:pointer;display:flex;font-size:.875rem;font-weight:500;gap:.5rem;justify-content:center;min-height:44px;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}.n-users-login-section button[type=submit]:hover:not(:disabled),.n-users-register-section button[type=submit]:hover:not(:disabled),.n-users-reset-password-container button[type=submit]:hover:not(:disabled),.n-users-user-form button[type=submit]:hover:not(:disabled),form.n-users-form button[type=submit]:hover:not(:disabled){background-color:var(--nu-color-primary-dark);border-color:var(--nu-color-primary-dark)}.n-users-login-section button[type=submit]:disabled,.n-users-register-section button[type=submit]:disabled,.n-users-reset-password-container button[type=submit]:disabled,.n-users-user-form button[type=submit]:disabled,form.n-users-form button[type=submit]:disabled{cursor:not-allowed;opacity:.6}.n-users-error-message,.n-users-error-text{background-color:var(--nu-color-error-light,#fef2f2);border:1px solid var(--nu-color-error-border,#fecaca);border-radius:.5rem;color:var(--nu-color-error,#dc3545);font-size:.875rem;margin-top:.5rem;padding:.75rem 1rem}.n-users-success-message{background-color:var(--nu-color-success-light,#f0fff4);border:1px solid var(--nu-color-success-border,#9ae6b4);border-radius:.5rem;color:var(--nu-color-success,#10b981);font-size:.875rem;margin-top:.5rem;padding:.75rem 1rem}.n-users-grid{display:grid;gap:1rem}.n-users-grid-2{grid-template-columns:1fr 5fr}.n-users-grid-auto{grid-template-columns:repeat(auto-fill,minmax(12em,1fr))}.n-users-form-actions{align-items:center;display:flex;gap:1rem;margin-top:1.5rem}.n-users-loading-spinner{animation:n-users-spin 1s linear infinite;border:2px solid var(--nu-color-white,#fff);border-radius:50%;border-top:2px solid transparent;height:16px;width:16px}@keyframes n-users-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.n-users-card{background-color:var(--nu-color-bg-primary,#fff);border:1px solid var(--nu-color-border,#e5e7eb);border-radius:.5rem;padding:1rem}.n-users-section{margin:0 auto;max-width:42em;padding:1em}.n-users-section-header{border-bottom:.1em solid var(--nu-color-border,#e5e7eb);margin-bottom:1rem;padding-bottom:.5rem}.n-users-form-help{color:var(--nu-color-text-secondary,#6b7280);font-size:.75rem;margin-top:.25rem}.n-users-strength-bar{background-color:var(--nu-color-strength-bg,#e9ecef);border-radius:2px;height:4px;margin-bottom:4px;overflow:hidden;width:100%}.n-users-strength-fill{height:100%;transition:width .3s ease,background-color .3s ease}.n-users-strength-text{font-size:.75rem;font-weight:500}.n-users-validation-container{background-color:#f8f9fa;border:1px solid #e9ecef;border-radius:4px;margin-top:12px;padding:12px}.n-users-container-title{color:#495057;font-size:.875rem;font-weight:600;margin-bottom:8px}.n-users-list-unstyled{list-style:none;margin:0;padding-left:0}.n-users-list-item{align-items:center;display:flex;font-size:.875rem;margin-bottom:6px;transition:color .2s ease}.n-users-list-item:last-child{margin-bottom:0}.n-users-icon{font-weight:700;margin-right:8px;min-width:16px;text-align:center}.n-users-text{flex:1}.n-users-list ul{list-style:none}.n-users-login-section form,.n-users-register-section form,.n-users-reset-password-container form,.n-users-user-form form,form.n-users-form{display:flex;flex-direction:column;gap:1.5rem}.n-users-login-section{background:linear-gradient(135deg,var(--nu-color-bg-primary) 0,var(--nu-color-bg-secondary) 100%);border:1px solid var(--nu-color-border);border-radius:.75em;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);max-width:25rem}.n-users-login-header{margin-bottom:1em;text-align:center}.n-users-login-title{color:var(--nu-color-gray-700);font-size:1.875rem;font-weight:700;line-height:1.2;margin:0 0 .5em}.n-users-login-subtitle{color:var(--nu-color-gray-700);font-size:.875rem;line-height:1.5;margin:0}.n-users-remember-me{align-items:center;display:flex;gap:.5rem}.n-users-remember-me label{color:var(--nu-color-gray-700);font-size:.875rem;margin:0}.n-users-login-footer{margin-top:1em;text-align:center}.n-users-forgot-password{margin:0}.n-users-forgot-link{align-items:center;color:var(--nu-color-primary);display:inline-flex;font-size:.875rem;gap:.5rem;text-decoration:none;transition:color .2s ease-in-out}.n-users-forgot-link:hover:not(.loading){color:var(--nu-color-primary-dark);text-decoration:underline}.n-users-forgot-link.loading{cursor:not-allowed;opacity:.6;pointer-events:none}.n-users-password-strength{margin-top:8px}.n-users-validation-rules{margin-top:12px}.n-users-rules-title{margin-bottom:8px}.n-users-rules-list{padding-left:0}.n-users-rule-item{margin-bottom:6px}.n-users-rule-item:last-child{margin-bottom:0}.n-users-rule-passed{color:var(--nu-color-validation-passed,#28a745)}.n-users-rule-failed{color:var(--nu-color-validation-failed,#dc3545)}.n-users-rule-icon{margin-right:8px}.n-users-rule-text{flex:1}.n-users-validation-errors{margin-top:8px}.n-users-error-text:last-child{margin-bottom:0}.n-users-password-hints{margin-top:8px}.n-users-hint-title{margin-bottom:8px}.n-users-hint-list{padding-left:16px}.n-users-hint-item{color:var(--nu-color-validation-hint,#6c757d);margin-bottom:4px}.n-users-hint-item:last-child{margin-bottom:0}.n-users-logout-link{background:var(--nu-color-error);border:2px solid transparent;border-color:var(--nu-color-error-dark);border-radius:.5em;color:#fff;padding:.5em 1em;text-decoration:none}.n-users-logout-link:hover{border-color:var(--nu-color-primary)}.n-users-user-card dd{margin:0}.n-users-user-card-actions{display:flex;gap:.5rem;margin-top:1rem}.n-users-user-form{margin:0 auto;max-width:500px}.n-users-form-group input.error{border-color:#dc3545}.n-users-profile-info{background-color:var(--nu-color-bg-primary);border-top-left-radius:.5em;border-top-right-radius:.5em}.n-users-profile-details{font-size:1rem;list-style:none;margin:0;padding:0}.n-users-reset-password-container{background-color:var(--nu-color-bg-primary);border-bottom-left-radius:.5em;border-bottom-right-radius:.5em}@media (max-width:480px){.n-users-login-title{font-size:1.5rem}}.n-users-list-enter-active,.n-users-list-leave-active,.n-users-list-move{transition:all .3s ease}.n-users-list-enter-from,.n-users-list-leave-to{opacity:0;transform:translateX(2rem)}.n-users-list-leave-active{position:absolute}.n-users-list-move{transition-timing-function:ease}
1
+ :root{--nu-color-primary:#3b82f6;--nu-color-primary-dark:#2563eb;--nu-color-primary-light:#60a5fa;--nu-color-white:#fff;--nu-color-gray-50:#f9fafb;--nu-color-gray-500:#6b7280;--nu-color-gray-600:#4b5563;--nu-color-gray-700:#374151;--nu-color-gray-900:#111827;--nu-color-success:#059669;--nu-color-success-light:#f0fdf4;--nu-color-success-border:#9ae6b4;--nu-color-error:#dc2626;--nu-color-error-light:#fef2f2;--nu-color-error-border:#fecaca;--nu-color-error-alt:#db5050;--nu-color-error-dark:#b91c1c;--nu-color-border:#e5e7eb;--nu-color-border-light:#d1d5db;--nu-color-border-dark:#ccc;--nu-color-bg-primary:#fff;--nu-color-bg-secondary:#f9fafb;--nu-color-bg-tertiary:#f3f4f6;--nu-color-text-primary:#000;--nu-color-text-secondary:#6b7280;--nu-color-strength-bg:#e9ecef;--nu-color-validation-passed:#28a745;--nu-color-validation-failed:#dc3545;--nu-color-validation-hint:#6c757d}.dark :root{--nu-color-primary:#60a5fa;--nu-color-primary-dark:#3b82f6;--nu-color-primary-light:#93c5fd;--nu-color-white:#000;--nu-color-gray-50:#111827;--nu-color-gray-500:#9ca3af;--nu-color-gray-600:#d1d5db;--nu-color-gray-700:#e5e7eb;--nu-color-gray-900:#f9fafb;--nu-color-success:#10b981;--nu-color-success-light:#064e3b;--nu-color-success-border:#065f46;--nu-color-error:#ef4444;--nu-color-error-light:#450a0a;--nu-color-error-border:#7f1d1d;--nu-color-error-alt:#f87171;--nu-color-error-dark:#dc2626;--nu-color-border:#374151;--nu-color-border-light:#4b5563;--nu-color-border-dark:#6b7280;--nu-color-bg-primary:#111827;--nu-color-bg-secondary:#1f2937;--nu-color-bg-tertiary:#374151;--nu-color-text-primary:#fff;--nu-color-text-secondary:#9ca3af;--nu-color-strength-bg:#374151;--nu-color-validation-passed:#22c55e;--nu-color-validation-failed:#ef4444;--nu-color-validation-hint:#9ca3af}.n-users-form-group{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;width:100%}.n-users-form-group.n-users-form-group-narrow{width:90%}.n-users-form-group label,.n-users-form-label{color:var(--nu-color-text-primary,#000);display:block;font-size:1rem;font-weight:500;margin-bottom:.5rem}.n-users-form-group input,.n-users-form-group select,.n-users-form-input{border:1px solid var(--nu-color-border,#ccc);border-radius:.25rem;font-size:1rem;padding:.5rem;transition:border-color .2s,box-shadow .2s;width:100%}.n-users-form-group input:focus,.n-users-form-group select:focus,.n-users-form-input:focus{border-color:var(--nu-color-primary);box-shadow:0 0 0 .1rem rgba(59,130,246,.1);outline:none}.n-users-form-group input:disabled,.n-users-form-input:disabled{background-color:var(--nu-color-bg-secondary,#f9fafb);cursor:not-allowed}.n-users-form-group input.error,.n-users-form-input.error{border-color:var(--nu-color-error,#dc3545)}.n-users-form-group input[type=email],.n-users-form-group input[type=password],.n-users-form-group input[type=text]{background-color:var(--nu-color-bg-secondary);border:1px solid var(--nu-color-border-light);border-radius:.5em;box-sizing:border-box;color:var(--nu-color-gray-700);font-size:.875rem;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}.n-users-form-group input[type=email]:focus,.n-users-form-group input[type=password]:focus,.n-users-form-group input[type=text]:focus{background-color:var(--nu-color-bg-primary);border-color:var(--nu-color-primary);box-shadow:0 0 0 3px rgba(59,130,246,.1);outline:none}.n-users-form-group input[type=checkbox]{margin:0;width:auto}.n-users-google-btn{align-items:center;display:inline-flex;gap:8px;justify-content:center}.n-users-google-logo{flex-shrink:0}.n-users-btn,.n-users-cancel-btn,.n-users-delete-btn,.n-users-edit-btn,.n-users-google-btn,.n-users-submit-btn{align-self:flex-start;border:none;border-radius:.25rem;cursor:pointer;display:inline-block;font-size:1rem;font-weight:500;padding:.5rem 1rem;text-decoration:none;transition:background-color .2s,transform .1s}.n-users-btn:hover,.n-users-edit-btn:hover,.n-users-submit-btn:hover{background-color:var(--nu-color-primary-dark,#2563eb);transform:translateY(-1px)}.n-users-btn:disabled,.n-users-submit-btn:disabled{cursor:not-allowed;opacity:.6;transform:none}.n-users-btn-primary,.n-users-edit-btn,.n-users-submit-btn{background-color:var(--nu-color-primary,#3b82f6);color:#fff}.n-users-btn-secondary,.n-users-cancel-btn{background-color:var(--nu-color-gray-500,#6b7280);color:#fff}.n-users-cancel-btn:hover{background-color:var(--nu-color-gray-600,#4b5563)}.n-users-delete-btn{background-color:var(--nu-color-error,#ef4444);color:#fff}.n-users-delete-btn:hover{background-color:var(--nu-color-error-alt,#dc2626)}.n-users-login-section button[type=submit],.n-users-register-section button[type=submit],.n-users-reset-password-container button[type=submit],.n-users-user-form button[type=submit],form.n-users-form button[type=submit]{align-items:center;background-color:var(--nu-color-primary);border:1px solid var(--nu-color-primary);border-radius:.5em;color:var(--nu-color-white);cursor:pointer;display:flex;font-size:.875rem;font-weight:500;gap:.5rem;justify-content:center;min-height:44px;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}.n-users-login-section button[type=submit]:hover:not(:disabled),.n-users-register-section button[type=submit]:hover:not(:disabled),.n-users-reset-password-container button[type=submit]:hover:not(:disabled),.n-users-user-form button[type=submit]:hover:not(:disabled),form.n-users-form button[type=submit]:hover:not(:disabled){background-color:var(--nu-color-primary-dark);border-color:var(--nu-color-primary-dark)}.n-users-login-section button[type=submit]:disabled,.n-users-register-section button[type=submit]:disabled,.n-users-reset-password-container button[type=submit]:disabled,.n-users-user-form button[type=submit]:disabled,form.n-users-form button[type=submit]:disabled{cursor:not-allowed;opacity:.6}.n-users-error-message,.n-users-error-text{background-color:var(--nu-color-error-light,#fef2f2);border:1px solid var(--nu-color-error-border,#fecaca);border-radius:.5rem;color:var(--nu-color-error,#dc3545);font-size:.875rem;margin-top:.5rem;padding:.75rem 1rem}.n-users-success-message{background-color:var(--nu-color-success-light,#f0fff4);border:1px solid var(--nu-color-success-border,#9ae6b4);border-radius:.5rem;color:var(--nu-color-success,#10b981);font-size:.875rem;margin-top:.5rem;padding:.75rem 1rem}.n-users-grid{display:grid;gap:1rem}.n-users-grid-2{grid-template-columns:1fr 5fr}.n-users-grid-auto{grid-template-columns:repeat(auto-fill,minmax(12em,1fr))}.n-users-form-actions{align-items:center;display:flex;gap:1rem;margin-top:1.5rem}.n-users-loading-spinner{animation:n-users-spin 1s linear infinite;border:2px solid var(--nu-color-white,#fff);border-radius:50%;border-top:2px solid transparent;height:16px;width:16px}@keyframes n-users-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.n-users-card{background-color:var(--nu-color-bg-primary,#fff);border:1px solid var(--nu-color-border,#e5e7eb);border-radius:.5rem;padding:1rem}.n-users-section{margin:0 auto;max-width:42em;padding:1em}.n-users-section-header{border-bottom:.1em solid var(--nu-color-border,#e5e7eb);margin-bottom:1rem;padding-bottom:.5rem}.n-users-form-help{color:var(--nu-color-text-secondary,#6b7280);font-size:.75rem;margin-top:.25rem}.n-users-strength-bar{background-color:var(--nu-color-strength-bg,#e9ecef);border-radius:2px;height:4px;margin-bottom:4px;overflow:hidden;width:100%}.n-users-strength-fill{height:100%;transition:width .3s ease,background-color .3s ease}.n-users-strength-text{font-size:.75rem;font-weight:500}.n-users-validation-container{background-color:#f8f9fa;border:1px solid #e9ecef;border-radius:4px;margin-top:12px;padding:12px}.n-users-container-title{color:#495057;font-size:.875rem;font-weight:600;margin-bottom:8px}.n-users-list-unstyled{list-style:none;margin:0;padding-left:0}.n-users-list-item{align-items:center;display:flex;font-size:.875rem;margin-bottom:6px;transition:color .2s ease}.n-users-list-item:last-child{margin-bottom:0}.n-users-icon{font-weight:700;margin-right:8px;min-width:16px;text-align:center}.n-users-text{flex:1}.n-users-list ul{list-style:none}.n-users-login-section form,.n-users-register-section form,.n-users-reset-password-container form,.n-users-user-form form,form.n-users-form{display:flex;flex-direction:column;gap:1.5rem}.n-users-login-section{background:linear-gradient(135deg,var(--nu-color-bg-primary) 0,var(--nu-color-bg-secondary) 100%);border:1px solid var(--nu-color-border);border-radius:.75em;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);max-width:25rem}.n-users-login-header{margin-bottom:1em;text-align:center}.n-users-login-title{color:var(--nu-color-gray-700);font-size:1.875rem;font-weight:700;line-height:1.2;margin:0 0 .5em}.n-users-login-subtitle{color:var(--nu-color-gray-700);font-size:.875rem;line-height:1.5;margin:0}.n-users-remember-me{align-items:center;display:flex;gap:.5rem}.n-users-remember-me label{color:var(--nu-color-gray-700);font-size:.875rem;margin:0}.n-users-login-footer{margin-top:1em;text-align:center}.n-users-forgot-password{margin:0}.n-users-forgot-link{align-items:center;color:var(--nu-color-primary);display:inline-flex;font-size:.875rem;gap:.5rem;text-decoration:none;transition:color .2s ease-in-out}.n-users-forgot-link:hover:not(.loading){color:var(--nu-color-primary-dark);text-decoration:underline}.n-users-forgot-link.loading{cursor:not-allowed;opacity:.6;pointer-events:none}.n-users-password-strength{margin-top:8px}.n-users-validation-rules{margin-top:12px}.n-users-rules-title{margin-bottom:8px}.n-users-rules-list{padding-left:0}.n-users-rule-item{margin-bottom:6px}.n-users-rule-item:last-child{margin-bottom:0}.n-users-rule-passed{color:var(--nu-color-validation-passed,#28a745)}.n-users-rule-failed{color:var(--nu-color-validation-failed,#dc3545)}.n-users-rule-icon{margin-right:8px}.n-users-rule-text{flex:1}.n-users-validation-errors{margin-top:8px}.n-users-error-text:last-child{margin-bottom:0}.n-users-password-hints{margin-top:8px}.n-users-hint-title{margin-bottom:8px}.n-users-hint-list{padding-left:16px}.n-users-hint-item{color:var(--nu-color-validation-hint,#6c757d);margin-bottom:4px}.n-users-hint-item:last-child{margin-bottom:0}.n-users-logout-link{background:var(--nu-color-error);border:2px solid transparent;border-color:var(--nu-color-error-dark);border-radius:.5em;color:#fff;padding:.5em 1em;text-decoration:none}.n-users-logout-link:hover{border-color:var(--nu-color-primary)}.n-users-user-card dd{margin:0}.n-users-user-card-actions{display:flex;gap:.5rem;margin-top:1rem}.n-users-user-form{margin:0 auto;max-width:500px}.n-users-form-group input.error{border-color:#dc3545}.n-users-profile-info{background-color:var(--nu-color-bg-primary);border-top-left-radius:.5em;border-top-right-radius:.5em}.n-users-profile-details{font-size:1rem;list-style:none;margin:0;padding:0}.n-users-reset-password-container{background-color:var(--nu-color-bg-primary);border-bottom-left-radius:.5em;border-bottom-right-radius:.5em}@media (max-width:480px){.n-users-login-title{font-size:1.5rem}}.n-users-list-enter-active,.n-users-list-leave-active,.n-users-list-move{transition:all .3s ease}.n-users-list-enter-from,.n-users-list-leave-to{opacity:0;transform:translateX(2rem)}.n-users-list-leave-active{position:absolute}.n-users-list-move{transition-timing-function:ease}
@@ -0,0 +1,75 @@
1
+ <script setup>
2
+ import { ref, computed } from "vue";
3
+ import { useRuntimeConfig, navigateTo } from "#imports";
4
+ const props = defineProps({
5
+ redirectEndpoint: { type: String, required: false },
6
+ buttonText: { type: String, required: false, default: "Continue with Google" },
7
+ showLogo: { type: Boolean, required: false, default: true },
8
+ class: { type: String, required: false }
9
+ });
10
+ const emit = defineEmits(["click"]);
11
+ const { public: { nuxtUsers } } = useRuntimeConfig();
12
+ const { apiBasePath } = nuxtUsers;
13
+ const isLoading = ref(false);
14
+ const redirectEndpoint = computed(
15
+ () => props.redirectEndpoint || `${apiBasePath}/auth/google/redirect`
16
+ );
17
+ const handleGoogleLogin = async () => {
18
+ isLoading.value = true;
19
+ emit("click");
20
+ try {
21
+ await navigateTo(redirectEndpoint.value, { external: true });
22
+ } catch (error) {
23
+ console.error("[Nuxt Users] Google OAuth redirect failed:", error);
24
+ } finally {
25
+ isLoading.value = false;
26
+ }
27
+ };
28
+ </script>
29
+
30
+ <template>
31
+ <button
32
+ type="button"
33
+ class="n-users-google-btn"
34
+ :class="[{ loading: isLoading }, props.class]"
35
+ :disabled="isLoading"
36
+ @click="handleGoogleLogin"
37
+ >
38
+ <!-- Loading spinner -->
39
+ <span
40
+ v-if="isLoading"
41
+ class="n-users-loading-spinner"
42
+ />
43
+
44
+ <!-- Google logo SVG -->
45
+ <svg
46
+ v-if="showLogo && !isLoading"
47
+ class="n-users-google-logo"
48
+ viewBox="0 0 24 24"
49
+ width="18"
50
+ height="18"
51
+ >
52
+ <path
53
+ fill="#4285F4"
54
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
55
+ />
56
+ <path
57
+ fill="#34A853"
58
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
59
+ />
60
+ <path
61
+ fill="#FBBC05"
62
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
63
+ />
64
+ <path
65
+ fill="#EA4335"
66
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
67
+ />
68
+ </svg>
69
+
70
+ <!-- Button text -->
71
+ <span class="n-users-google-text">
72
+ {{ isLoading ? "Redirecting..." : buttonText }}
73
+ </span>
74
+ </button>
75
+ </template>
@@ -0,0 +1,30 @@
1
+ interface Props {
2
+ /**
3
+ * Custom redirect endpoint for Google OAuth
4
+ * @default '/api/nuxt-users/auth/google/redirect'
5
+ */
6
+ redirectEndpoint?: string;
7
+ /**
8
+ * Button text
9
+ * @default 'Continue with Google'
10
+ */
11
+ buttonText?: string;
12
+ /**
13
+ * Show Google logo in button
14
+ * @default true
15
+ */
16
+ showLogo?: boolean;
17
+ /**
18
+ * Custom CSS class for button
19
+ */
20
+ class?: string;
21
+ }
22
+ declare const _default: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
23
+ click: () => any;
24
+ }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
25
+ onClick?: (() => any) | undefined;
26
+ }>, {
27
+ buttonText: string;
28
+ showLogo: boolean;
29
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
+ export default _default;
@@ -4,6 +4,6 @@ export declare const useAuthentication: () => {
4
4
  isAuthenticated: import("vue").ComputedRef<boolean>;
5
5
  login: (userData: User, rememberMe?: boolean) => void;
6
6
  logout: () => Promise<void>;
7
- fetchUser: () => Promise<any>;
7
+ fetchUser: (useSSR?: boolean) => Promise<any>;
8
8
  initializeUser: () => Promise<void>;
9
9
  };
@@ -1,4 +1,4 @@
1
- import { useState, useRuntimeConfig } from "#app";
1
+ import { useState, useRuntimeConfig, useFetch } from "#app";
2
2
  import { computed, readonly } from "vue";
3
3
  let initializationPromise = null;
4
4
  export const useAuthentication = () => {
@@ -36,22 +36,40 @@ export const useAuthentication = () => {
36
36
  }
37
37
  }
38
38
  };
39
- const fetchUser = async () => {
39
+ const fetchUser = async (useSSR = false) => {
40
40
  try {
41
- const response = await $fetch(`${apiBasePath}/me`, { method: "GET" });
42
- user.value = response.user;
41
+ let userData = null;
42
+ if (useSSR) {
43
+ const { data, error } = await useFetch(`${apiBasePath}/me`, {
44
+ server: true,
45
+ lazy: false
46
+ });
47
+ if (error.value) {
48
+ throw new Error(error.value.message || "Failed to fetch user");
49
+ }
50
+ userData = data.value?.user || null;
51
+ } else {
52
+ const response = await $fetch(`${apiBasePath}/me`, {
53
+ method: "GET"
54
+ });
55
+ userData = response.user;
56
+ }
57
+ if (!userData) {
58
+ throw new Error("No user data received");
59
+ }
60
+ user.value = userData;
43
61
  if (import.meta.client) {
44
62
  const wasInLocalStorage = localStorage.getItem("user") !== null;
45
63
  const wasInSessionStorage = sessionStorage.getItem("user") !== null;
46
64
  if (wasInLocalStorage) {
47
- localStorage.setItem("user", JSON.stringify(response.user));
65
+ localStorage.setItem("user", JSON.stringify(userData));
48
66
  } else if (wasInSessionStorage) {
49
- sessionStorage.setItem("user", JSON.stringify(response.user));
67
+ sessionStorage.setItem("user", JSON.stringify(userData));
50
68
  } else {
51
- sessionStorage.setItem("user", JSON.stringify(response.user));
69
+ sessionStorage.setItem("user", JSON.stringify(userData));
52
70
  }
53
71
  }
54
- return response.user;
72
+ return userData;
55
73
  } catch (error) {
56
74
  console.error("Failed to fetch user:", error);
57
75
  user.value = null;
@@ -2,7 +2,7 @@ import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from "#app";
2
2
  import { useAuthentication } from "../composables/useAuthentication.js";
3
3
  import { hasPermission, isWhitelisted } from "../utils/permissions.js";
4
4
  import { NO_AUTH_PATHS, NO_AUTH_API_PATHS } from "../constants.js";
5
- export default defineNuxtRouteMiddleware((to) => {
5
+ export default defineNuxtRouteMiddleware(async (to) => {
6
6
  const { public: { nuxtUsers: publicNuxtUsers } } = useRuntimeConfig();
7
7
  const publicOptions = publicNuxtUsers;
8
8
  const base = publicOptions.apiBasePath || "/api/nuxt-users";
@@ -18,7 +18,14 @@ export default defineNuxtRouteMiddleware((to) => {
18
18
  console.log(`[Nuxt Users] client.middleware.auth.global: Whitelisted: ${to.path}`);
19
19
  return;
20
20
  }
21
- const { isAuthenticated, user } = useAuthentication();
21
+ const { isAuthenticated, user, fetchUser } = useAuthentication();
22
+ if (!isAuthenticated.value && to.query?.oauth_success === "true") {
23
+ try {
24
+ await fetchUser(true);
25
+ } catch (error) {
26
+ console.error("[Nuxt Users] Failed to fetch user after OAuth:", error);
27
+ }
28
+ }
22
29
  if (!isAuthenticated.value) {
23
30
  console.log(`[Nuxt Users] client.middleware.auth.global: Unauthenticated ${to.path}, redirecting to /login`);
24
31
  return navigateTo("/login");
@@ -27,9 +34,4 @@ export default defineNuxtRouteMiddleware((to) => {
27
34
  console.log(`[Nuxt Users] client.middleware.auth.global: User with role ${user.value?.role} denied access to ${to.path}`);
28
35
  return navigateTo("/login");
29
36
  }
30
- console.log("[Nuxt Users] client.middleware.auth.global", {
31
- isAuthenticated: isAuthenticated.value,
32
- userRole: user.value?.role,
33
- to: to.path
34
- });
35
37
  });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
2
+ export default _default;
@@ -0,0 +1,68 @@
1
+ import { defineEventHandler, sendRedirect, getQuery, setCookie } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import {
4
+ createGoogleOAuth2Client,
5
+ getGoogleUserFromCode,
6
+ findOrCreateGoogleUser,
7
+ createAuthTokenForUser
8
+ } from "../../../../utils/google-oauth.js";
9
+ export default defineEventHandler(async (event) => {
10
+ const { nuxtUsers } = useRuntimeConfig();
11
+ const options = nuxtUsers;
12
+ const query = getQuery(event);
13
+ if (!options.auth.google) {
14
+ return sendRedirect(event, "/login?error=oauth_not_configured");
15
+ }
16
+ if (query.error) {
17
+ console.error("[Nuxt Users] Google OAuth error:", query.error);
18
+ const errorRedirect = options.auth.google.errorRedirect || "/login?error=oauth_failed";
19
+ return sendRedirect(event, errorRedirect);
20
+ }
21
+ if (!query.code || typeof query.code !== "string") {
22
+ console.error("[Nuxt Users] Missing authorization code in OAuth callback");
23
+ const errorRedirect = options.auth.google.errorRedirect || "/login?error=oauth_failed";
24
+ return sendRedirect(event, errorRedirect);
25
+ }
26
+ try {
27
+ const baseUrl = getRequestURL(event).origin;
28
+ const callbackPath = options.auth.google.callbackUrl || `${options.apiBasePath}/auth/google/callback`;
29
+ const callbackUrl = `${baseUrl}${callbackPath}`;
30
+ const oauth2Client = createGoogleOAuth2Client(options.auth.google, callbackUrl);
31
+ const googleUser = await getGoogleUserFromCode(oauth2Client, query.code);
32
+ const user = await findOrCreateGoogleUser(googleUser, options);
33
+ if (!user) {
34
+ console.warn(`[Nuxt Users] User not registered attempted Google OAuth login: ${googleUser.email}`);
35
+ const errorRedirect = options.auth.google.errorRedirect || "/login?error=user_not_registered";
36
+ return sendRedirect(event, errorRedirect);
37
+ }
38
+ if (!user.active) {
39
+ console.warn(`[Nuxt Users] Inactive user attempted Google OAuth login: ${user.email}`);
40
+ const errorRedirect = options.auth.google.errorRedirect || "/login?error=account_inactive";
41
+ return sendRedirect(event, errorRedirect);
42
+ }
43
+ const token = await createAuthTokenForUser(user, options, true);
44
+ const cookieOptions = {
45
+ httpOnly: true,
46
+ secure: process.env.NODE_ENV === "production",
47
+ sameSite: "lax",
48
+ path: "/",
49
+ maxAge: 60 * 60 * 24 * (options.auth.rememberMeExpiration || 30)
50
+ // 30 days default
51
+ };
52
+ setCookie(event, "auth_token", token, cookieOptions);
53
+ console.log(`[Nuxt Users] Google OAuth login successful for user: ${user.email}`);
54
+ const successRedirect = options.auth.google.successRedirect || "/";
55
+ const redirectUrl = successRedirect.includes("?") ? `${successRedirect}&oauth_success=true` : `${successRedirect}?oauth_success=true`;
56
+ return sendRedirect(event, redirectUrl);
57
+ } catch (error) {
58
+ console.error("[Nuxt Users] Google OAuth callback error:", error);
59
+ const errorRedirect = options.auth.google?.errorRedirect || "/login?error=oauth_failed";
60
+ return sendRedirect(event, errorRedirect);
61
+ }
62
+ });
63
+ const getRequestURL = (event) => {
64
+ const headers = event.node.req.headers;
65
+ const host = headers.host || headers[":authority"];
66
+ const protocol = headers["x-forwarded-proto"] || "https";
67
+ return new URL(`${protocol}://${host}`);
68
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
2
+ export default _default;
@@ -0,0 +1,39 @@
1
+ import { defineEventHandler, sendRedirect, createError } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { createGoogleOAuth2Client, getGoogleAuthUrl } from "../../../../utils/google-oauth.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const { nuxtUsers } = useRuntimeConfig();
6
+ const options = nuxtUsers;
7
+ if (!options.auth.google) {
8
+ throw createError({
9
+ statusCode: 500,
10
+ statusMessage: "Google OAuth is not configured"
11
+ });
12
+ }
13
+ if (!options.auth.google.clientId || !options.auth.google.clientSecret) {
14
+ throw createError({
15
+ statusCode: 500,
16
+ statusMessage: "Google OAuth client ID and secret are required"
17
+ });
18
+ }
19
+ try {
20
+ const baseUrl = getRequestURL(event).origin;
21
+ const callbackPath = options.auth.google.callbackUrl || `${options.apiBasePath}/auth/google/callback`;
22
+ const callbackUrl = `${baseUrl}${callbackPath}`;
23
+ const oauth2Client = createGoogleOAuth2Client(options.auth.google, callbackUrl);
24
+ const authUrl = getGoogleAuthUrl(oauth2Client, options.auth.google);
25
+ return sendRedirect(event, authUrl);
26
+ } catch (error) {
27
+ console.error("[Nuxt Users] Google OAuth redirect error:", error);
28
+ throw createError({
29
+ statusCode: 500,
30
+ statusMessage: "Failed to initiate Google OAuth"
31
+ });
32
+ }
33
+ });
34
+ const getRequestURL = (event) => {
35
+ const headers = event.node.req.headers;
36
+ const host = headers.host || headers[":authority"];
37
+ const protocol = headers["x-forwarded-proto"];
38
+ return new URL(`${protocol}://${host}`);
39
+ };
@@ -0,0 +1,2 @@
1
+ import type { ModuleOptions } from 'nuxt-users/utils';
2
+ export declare const addGoogleOauthFields: (options: ModuleOptions) => Promise<void>;
@@ -0,0 +1,22 @@
1
+ import { useDb } from "./db.js";
2
+ export const addGoogleOauthFields = async (options) => {
3
+ const connectorName = options.connector.name;
4
+ const db = await useDb(options);
5
+ const tableName = options.tables.users;
6
+ console.log(`[Nuxt Users] DB:Add Google OAuth fields to ${connectorName} Users Table in ${tableName}...`);
7
+ if (connectorName === "sqlite" || connectorName === "mysql" || connectorName === "postgresql") {
8
+ try {
9
+ await db.sql`ALTER TABLE {${tableName}} ADD COLUMN google_id TEXT`;
10
+ console.log("[Nuxt Users] Added google_id column \u2705");
11
+ } catch (error) {
12
+ console.error("[Nuxt Users] google_id column might already exist", error);
13
+ }
14
+ try {
15
+ await db.sql`ALTER TABLE {${tableName}} ADD COLUMN profile_picture TEXT`;
16
+ console.log("[Nuxt Users] Added profile_picture column \u2705");
17
+ } catch (error) {
18
+ console.error("[Nuxt Users] profile_picture column might already exist", error);
19
+ }
20
+ }
21
+ console.log(`[Nuxt Users] DB:Add Google OAuth fields to ${connectorName} Users Table \u2705`);
22
+ };
@@ -12,6 +12,8 @@ export const createUsersTable = async (options) => {
12
12
  name TEXT NOT NULL,
13
13
  password TEXT NOT NULL,
14
14
  role TEXT NOT NULL DEFAULT 'user',
15
+ google_id TEXT UNIQUE,
16
+ profile_picture TEXT,
15
17
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
16
18
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
17
19
  )
@@ -25,6 +27,8 @@ export const createUsersTable = async (options) => {
25
27
  name VARCHAR(255) NOT NULL,
26
28
  password VARCHAR(255) NOT NULL,
27
29
  role VARCHAR(32) NOT NULL DEFAULT 'user',
30
+ google_id VARCHAR(255) UNIQUE,
31
+ profile_picture TEXT,
28
32
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
29
33
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
30
34
  )
@@ -38,10 +42,12 @@ export const createUsersTable = async (options) => {
38
42
  name VARCHAR(255) NOT NULL,
39
43
  password VARCHAR(255) NOT NULL,
40
44
  role VARCHAR(32) NOT NULL DEFAULT 'user',
45
+ google_id VARCHAR(255) UNIQUE,
46
+ profile_picture TEXT,
41
47
  created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
42
48
  updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
43
49
  )
44
50
  `;
45
51
  }
46
- console.log(`[Nuxt Users] DB:Create ${connectorName} Users Table Fields: id, email, name, password, role, created_at, updated_at \u2705`);
52
+ console.log(`[Nuxt Users] DB:Create ${connectorName} Users Table Fields: id, email, name, password, role, google_id, profile_picture, created_at, updated_at \u2705`);
47
53
  };
@@ -0,0 +1,33 @@
1
+ import { OAuth2Client } from 'google-auth-library';
2
+ import type { ModuleOptions, User, GoogleOAuthOptions } from 'nuxt-users/utils';
3
+ export interface GoogleUserInfo {
4
+ id: string;
5
+ email: string;
6
+ name: string;
7
+ picture?: string;
8
+ verified_email: boolean;
9
+ }
10
+ /**
11
+ * Create Google OAuth2 client
12
+ */
13
+ export declare function createGoogleOAuth2Client(options: GoogleOAuthOptions, callbackUrl: string): OAuth2Client;
14
+ /**
15
+ * Generate authorization URL for Google OAuth
16
+ */
17
+ export declare function getGoogleAuthUrl(oauth2Client: OAuth2Client, options: GoogleOAuthOptions): string;
18
+ /**
19
+ * Exchange authorization code for tokens and get user info
20
+ */
21
+ export declare function getGoogleUserFromCode(oauth2Client: OAuth2Client, code: string): Promise<GoogleUserInfo>;
22
+ /**
23
+ * Generate a secure random password for OAuth users
24
+ */
25
+ export declare function generateSecurePassword(): Promise<string>;
26
+ /**
27
+ * Find or create user from Google OAuth data
28
+ */
29
+ export declare function findOrCreateGoogleUser(googleUser: GoogleUserInfo, moduleOptions: ModuleOptions): Promise<User | null>;
30
+ /**
31
+ * Create authentication token for user (reusing existing logic)
32
+ */
33
+ export declare function createAuthTokenForUser(user: User, moduleOptions: ModuleOptions, rememberMe?: boolean): Promise<string>;
@@ -0,0 +1,136 @@
1
+ import { OAuth2Client } from "google-auth-library";
2
+ import crypto from "node:crypto";
3
+ import bcrypt from "bcrypt";
4
+ import { useDb } from "./db.js";
5
+ export function createGoogleOAuth2Client(options, callbackUrl) {
6
+ return new OAuth2Client(
7
+ options.clientId,
8
+ options.clientSecret,
9
+ callbackUrl
10
+ );
11
+ }
12
+ export function getGoogleAuthUrl(oauth2Client, options) {
13
+ const scopes = options.scopes || ["openid", "profile", "email"];
14
+ return oauth2Client.generateAuthUrl({
15
+ access_type: "offline",
16
+ scope: scopes,
17
+ include_granted_scopes: true
18
+ });
19
+ }
20
+ export async function getGoogleUserFromCode(oauth2Client, code) {
21
+ const { tokens } = await oauth2Client.getToken(code);
22
+ oauth2Client.setCredentials(tokens);
23
+ const response = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
24
+ headers: {
25
+ Authorization: `Bearer ${tokens.access_token}`
26
+ }
27
+ });
28
+ if (!response.ok) {
29
+ throw new Error(`Failed to fetch user info: ${response.statusText}`);
30
+ }
31
+ const data = await response.json();
32
+ if (!data.email || !data.verified_email) {
33
+ throw new Error("Google account email not verified");
34
+ }
35
+ return {
36
+ id: data.id,
37
+ email: data.email,
38
+ name: data.name || data.email.split("@")[0],
39
+ picture: data.picture || void 0,
40
+ verified_email: data.verified_email
41
+ };
42
+ }
43
+ export async function generateSecurePassword() {
44
+ const randomBytes = crypto.randomBytes(32).toString("hex");
45
+ return await bcrypt.hash(`OAUTH_${randomBytes}`, 10);
46
+ }
47
+ export async function findOrCreateGoogleUser(googleUser, moduleOptions) {
48
+ const db = await useDb(moduleOptions);
49
+ const usersTable = moduleOptions.tables.users;
50
+ const allowAutoRegistration = moduleOptions.auth.google?.allowAutoRegistration ?? false;
51
+ let userResult = await db.sql`
52
+ SELECT * FROM {${usersTable}}
53
+ WHERE google_id = ${googleUser.id}
54
+ `;
55
+ if (userResult.rows.length > 0) {
56
+ const user = userResult.rows[0];
57
+ if (user.profile_picture !== googleUser.picture) {
58
+ await db.sql`
59
+ UPDATE {${usersTable}}
60
+ SET profile_picture = ${googleUser.picture}, updated_at = CURRENT_TIMESTAMP
61
+ WHERE id = ${user.id}
62
+ `;
63
+ user.profile_picture = googleUser.picture;
64
+ }
65
+ return user;
66
+ }
67
+ userResult = await db.sql`
68
+ SELECT * FROM {${usersTable}}
69
+ WHERE email = ${googleUser.email}
70
+ `;
71
+ if (userResult.rows.length > 0) {
72
+ const user = userResult.rows[0];
73
+ await db.sql`
74
+ UPDATE {${usersTable}}
75
+ SET google_id = ${googleUser.id},
76
+ profile_picture = ${googleUser.picture},
77
+ updated_at = CURRENT_TIMESTAMP
78
+ WHERE id = ${user.id}
79
+ `;
80
+ user.google_id = googleUser.id;
81
+ user.profile_picture = googleUser.picture;
82
+ return user;
83
+ }
84
+ if (!allowAutoRegistration) {
85
+ return null;
86
+ }
87
+ const securePassword = await generateSecurePassword();
88
+ await db.sql`
89
+ INSERT INTO {${usersTable}} (
90
+ email, name, password, role, google_id, profile_picture,
91
+ active, created_at, updated_at
92
+ ) VALUES (
93
+ ${googleUser.email},
94
+ ${googleUser.name},
95
+ ${securePassword},
96
+ 'user',
97
+ ${googleUser.id},
98
+ ${googleUser.picture || null},
99
+ true,
100
+ CURRENT_TIMESTAMP,
101
+ CURRENT_TIMESTAMP
102
+ )
103
+ `;
104
+ const createdUserResult = await db.sql`
105
+ SELECT * FROM {${usersTable}}
106
+ WHERE google_id = ${googleUser.id}
107
+ `;
108
+ return createdUserResult.rows[0];
109
+ }
110
+ export async function createAuthTokenForUser(user, moduleOptions, rememberMe = false) {
111
+ const db = await useDb(moduleOptions);
112
+ const personalAccessTokensTable = moduleOptions.tables.personalAccessTokens;
113
+ const token = crypto.randomBytes(64).toString("hex");
114
+ const tokenName = "oauth_auth_token";
115
+ const expiresAt = /* @__PURE__ */ new Date();
116
+ if (rememberMe) {
117
+ const longTermDays = moduleOptions.auth.rememberMeExpiration || 30;
118
+ expiresAt.setDate(expiresAt.getDate() + longTermDays);
119
+ } else {
120
+ expiresAt.setMinutes(expiresAt.getMinutes() + moduleOptions.auth.tokenExpiration);
121
+ }
122
+ await db.sql`
123
+ INSERT INTO {${personalAccessTokensTable}} (
124
+ tokenable_type, tokenable_id, name, token, expires_at, created_at, updated_at
125
+ ) VALUES (
126
+ 'user',
127
+ ${user.id},
128
+ ${tokenName},
129
+ ${token},
130
+ ${expiresAt.toISOString().slice(0, 19).replace("T", " ")},
131
+ CURRENT_TIMESTAMP,
132
+ CURRENT_TIMESTAMP
133
+ )
134
+ `;
135
+ return token;
136
+ }
@@ -6,3 +6,4 @@ export { createPersonalAccessTokensTable } from './create-personal-access-tokens
6
6
  export { createPasswordResetTokensTable } from './create-password-reset-tokens-table.js';
7
7
  export { createMigrationsTable } from './create-migrations-table.js';
8
8
  export { runMigrations } from './migrate.js';
9
+ export { createGoogleOAuth2Client, getGoogleAuthUrl, getGoogleUserFromCode, findOrCreateGoogleUser, createAuthTokenForUser, generateSecurePassword } from './google-oauth.js';
@@ -6,3 +6,4 @@ export { createPersonalAccessTokensTable } from "./create-personal-access-tokens
6
6
  export { createPasswordResetTokensTable } from "./create-password-reset-tokens-table.js";
7
7
  export { createMigrationsTable } from "./create-migrations-table.js";
8
8
  export { runMigrations } from "./migrate.js";
9
+ export { createGoogleOAuth2Client, getGoogleAuthUrl, getGoogleUserFromCode, findOrCreateGoogleUser, createAuthTokenForUser, generateSecurePassword } from "./google-oauth.js";
@@ -4,6 +4,7 @@ import { createPersonalAccessTokensTable } from "./create-personal-access-tokens
4
4
  import { createPasswordResetTokensTable } from "./create-password-reset-tokens-table.js";
5
5
  import { createMigrationsTable } from "./create-migrations-table.js";
6
6
  import { addActiveToUsers } from "./add-active-to-users.js";
7
+ import { addGoogleOauthFields } from "./add-google-oauth-fields.js";
7
8
  const migrations = [
8
9
  {
9
10
  name: "create_migrations_table",
@@ -24,6 +25,10 @@ const migrations = [
24
25
  {
25
26
  name: "add_active_to_users",
26
27
  run: addActiveToUsers
28
+ },
29
+ {
30
+ name: "add_google_oauth_fields",
31
+ run: addGoogleOauthFields
27
32
  }
28
33
  ];
29
34
  export const getAppliedMigrations = async (options) => {
@@ -12,6 +12,42 @@ type DatabaseConfig = {
12
12
  password?: string;
13
13
  database?: string;
14
14
  };
15
+ interface GoogleOAuthOptions {
16
+ /**
17
+ * Google OAuth client ID from Google Cloud Console
18
+ */
19
+ clientId: string;
20
+ /**
21
+ * Google OAuth client secret from Google Cloud Console
22
+ */
23
+ clientSecret: string;
24
+ /**
25
+ * Callback URL for Google OAuth (must match what's configured in Google Cloud Console)
26
+ * @default '/api/nuxt-users/auth/google/callback'
27
+ */
28
+ callbackUrl?: string;
29
+ /**
30
+ * Redirect URL after successful authentication
31
+ * @default '/'
32
+ */
33
+ successRedirect?: string;
34
+ /**
35
+ * Redirect URL after failed authentication
36
+ * @default '/login?error=oauth_failed'
37
+ */
38
+ errorRedirect?: string;
39
+ /**
40
+ * Google OAuth scopes to request
41
+ * @default ['openid', 'profile', 'email']
42
+ */
43
+ scopes?: string[];
44
+ /**
45
+ * Allow automatic user registration when logging in with Google for the first time
46
+ * If false, only existing users with matching email can log in with Google
47
+ * @default false
48
+ */
49
+ allowAutoRegistration?: boolean;
50
+ }
15
51
  interface RuntimeModuleOptions {
16
52
  connector?: {
17
53
  name: DatabaseType;
@@ -75,6 +111,11 @@ interface RuntimeModuleOptions {
75
111
  * }
76
112
  */
77
113
  permissions?: Record<string, (string | Permission)[]>;
114
+ /**
115
+ * Google OAuth configuration
116
+ * Enable Google OAuth login/registration
117
+ */
118
+ google?: GoogleOAuthOptions;
78
119
  };
79
120
  /**
80
121
  * Password validation configuration
@@ -139,6 +180,7 @@ interface ModuleOptions {
139
180
  tokenExpiration: number;
140
181
  rememberMeExpiration: number;
141
182
  permissions: Record<string, (string | Permission)[]>;
183
+ google?: GoogleOAuthOptions;
142
184
  };
143
185
  passwordValidation: {
144
186
  minLength: number;
@@ -169,6 +211,8 @@ interface User {
169
211
  password: string;
170
212
  role: string;
171
213
  active: boolean;
214
+ google_id?: string;
215
+ profile_picture?: string;
172
216
  created_at: string;
173
217
  updated_at: string;
174
218
  last_login_at?: string;
@@ -225,4 +269,4 @@ declare const defaultFieldLabels: {
225
269
  };
226
270
 
227
271
  export { defaultDisplayFields as h, defaultFieldLabels as i };
228
- export type { DisplayFieldsProps as D, LoginFormData as L, ModuleOptions as M, PersonalAccessToken as P, RuntimeModuleOptions as R, User as U, UserWithoutPassword as a, PasswordResetToken as b, LoginFormProps as c, ResetPasswordFormProps as d, Permission as e, DatabaseType as f, DatabaseConfig as g };
272
+ export type { DisplayFieldsProps as D, GoogleOAuthOptions as G, LoginFormData as L, ModuleOptions as M, PersonalAccessToken as P, RuntimeModuleOptions as R, User as U, UserWithoutPassword as a, PasswordResetToken as b, LoginFormProps as c, ResetPasswordFormProps as d, Permission as e, DatabaseType as f, DatabaseConfig as g };
@@ -12,6 +12,42 @@ type DatabaseConfig = {
12
12
  password?: string;
13
13
  database?: string;
14
14
  };
15
+ interface GoogleOAuthOptions {
16
+ /**
17
+ * Google OAuth client ID from Google Cloud Console
18
+ */
19
+ clientId: string;
20
+ /**
21
+ * Google OAuth client secret from Google Cloud Console
22
+ */
23
+ clientSecret: string;
24
+ /**
25
+ * Callback URL for Google OAuth (must match what's configured in Google Cloud Console)
26
+ * @default '/api/nuxt-users/auth/google/callback'
27
+ */
28
+ callbackUrl?: string;
29
+ /**
30
+ * Redirect URL after successful authentication
31
+ * @default '/'
32
+ */
33
+ successRedirect?: string;
34
+ /**
35
+ * Redirect URL after failed authentication
36
+ * @default '/login?error=oauth_failed'
37
+ */
38
+ errorRedirect?: string;
39
+ /**
40
+ * Google OAuth scopes to request
41
+ * @default ['openid', 'profile', 'email']
42
+ */
43
+ scopes?: string[];
44
+ /**
45
+ * Allow automatic user registration when logging in with Google for the first time
46
+ * If false, only existing users with matching email can log in with Google
47
+ * @default false
48
+ */
49
+ allowAutoRegistration?: boolean;
50
+ }
15
51
  interface RuntimeModuleOptions {
16
52
  connector?: {
17
53
  name: DatabaseType;
@@ -75,6 +111,11 @@ interface RuntimeModuleOptions {
75
111
  * }
76
112
  */
77
113
  permissions?: Record<string, (string | Permission)[]>;
114
+ /**
115
+ * Google OAuth configuration
116
+ * Enable Google OAuth login/registration
117
+ */
118
+ google?: GoogleOAuthOptions;
78
119
  };
79
120
  /**
80
121
  * Password validation configuration
@@ -139,6 +180,7 @@ interface ModuleOptions {
139
180
  tokenExpiration: number;
140
181
  rememberMeExpiration: number;
141
182
  permissions: Record<string, (string | Permission)[]>;
183
+ google?: GoogleOAuthOptions;
142
184
  };
143
185
  passwordValidation: {
144
186
  minLength: number;
@@ -169,6 +211,8 @@ interface User {
169
211
  password: string;
170
212
  role: string;
171
213
  active: boolean;
214
+ google_id?: string;
215
+ profile_picture?: string;
172
216
  created_at: string;
173
217
  updated_at: string;
174
218
  last_login_at?: string;
@@ -225,4 +269,4 @@ declare const defaultFieldLabels: {
225
269
  };
226
270
 
227
271
  export { defaultDisplayFields as h, defaultFieldLabels as i };
228
- export type { DisplayFieldsProps as D, LoginFormData as L, ModuleOptions as M, PersonalAccessToken as P, RuntimeModuleOptions as R, User as U, UserWithoutPassword as a, PasswordResetToken as b, LoginFormProps as c, ResetPasswordFormProps as d, Permission as e, DatabaseType as f, DatabaseConfig as g };
272
+ export type { DisplayFieldsProps as D, GoogleOAuthOptions as G, LoginFormData as L, ModuleOptions as M, PersonalAccessToken as P, RuntimeModuleOptions as R, User as U, UserWithoutPassword as a, PasswordResetToken as b, LoginFormProps as c, ResetPasswordFormProps as d, Permission as e, DatabaseType as f, DatabaseConfig as g };
package/dist/utils.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { g as DatabaseConfig, f as DatabaseType, D as DisplayFieldsProps, L as LoginFormData, c as LoginFormProps, M as ModuleOptions, b as PasswordResetToken, e as Permission, P as PersonalAccessToken, d as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, a as UserWithoutPassword, h as defaultDisplayFields, i as defaultFieldLabels } from './shared/nuxt-users.BTVH9EyH.mjs';
1
+ export { g as DatabaseConfig, f as DatabaseType, D as DisplayFieldsProps, G as GoogleOAuthOptions, L as LoginFormData, c as LoginFormProps, M as ModuleOptions, b as PasswordResetToken, e as Permission, P as PersonalAccessToken, d as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, a as UserWithoutPassword, h as defaultDisplayFields, i as defaultFieldLabels } from './shared/nuxt-users.SXaMnaEL.mjs';
2
2
 
3
3
  interface PasswordValidationResult {
4
4
  isValid: boolean;
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { g as DatabaseConfig, f as DatabaseType, D as DisplayFieldsProps, L as LoginFormData, c as LoginFormProps, M as ModuleOptions, b as PasswordResetToken, e as Permission, P as PersonalAccessToken, d as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, a as UserWithoutPassword, h as defaultDisplayFields, i as defaultFieldLabels } from './shared/nuxt-users.BTVH9EyH.js';
1
+ export { g as DatabaseConfig, f as DatabaseType, D as DisplayFieldsProps, G as GoogleOAuthOptions, L as LoginFormData, c as LoginFormProps, M as ModuleOptions, b as PasswordResetToken, e as Permission, P as PersonalAccessToken, d as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, a as UserWithoutPassword, h as defaultDisplayFields, i as defaultFieldLabels } from './shared/nuxt-users.SXaMnaEL.js';
2
2
 
3
3
  interface PasswordValidationResult {
4
4
  isValid: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-users",
3
- "version": "1.34.7",
3
+ "version": "1.35.0",
4
4
  "description": "A comprehensive user management module for Nuxt 3 and Nuxt 4 applications with authentication, authorization, database support, and CLI tools",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -92,6 +92,7 @@
92
92
  "bcrypt": "^6.0.0",
93
93
  "citty": "^0.1.6",
94
94
  "defu": "^6.1.4",
95
+ "google-auth-library": "^10.4.0",
95
96
  "nodemailer": "^7.0.5"
96
97
  },
97
98
  "optionalDependencies": {