@wpnuxt/auth 2.0.0-alpha.1 → 2.0.0-alpha.3

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 (27) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +194 -4
  3. package/dist/runtime/composables/useWPAuth.js +113 -12
  4. package/dist/runtime/composables/useWPUser.js +2 -1
  5. package/dist/runtime/plugins/auth.js +10 -0
  6. package/dist/runtime/queries/Auth.gql +111 -5
  7. package/dist/runtime/queries/Viewer.gql +26 -0
  8. package/dist/runtime/queries/fragments/GeneralSettings.fragment.gql +11 -0
  9. package/dist/runtime/server/api/auth/login.post.d.ts +0 -0
  10. package/dist/runtime/server/api/auth/login.post.js +51 -0
  11. package/dist/runtime/server/api/auth/logout.post.d.ts +0 -0
  12. package/dist/runtime/server/api/auth/logout.post.js +18 -0
  13. package/dist/runtime/server/api/auth/oauth/authorize.get.d.ts +0 -0
  14. package/dist/runtime/server/api/auth/oauth/authorize.get.js +36 -0
  15. package/dist/runtime/server/api/auth/oauth/callback.get.d.ts +0 -0
  16. package/dist/runtime/server/api/auth/oauth/callback.get.js +106 -0
  17. package/dist/runtime/server/api/auth/provider/[provider]/authorize.get.d.ts +0 -0
  18. package/dist/runtime/server/api/auth/provider/[provider]/authorize.get.js +82 -0
  19. package/dist/runtime/server/api/auth/provider/[provider]/callback.get.d.ts +0 -0
  20. package/dist/runtime/server/api/auth/provider/[provider]/callback.get.js +136 -0
  21. package/dist/runtime/server/utils/logger.d.ts +0 -0
  22. package/dist/runtime/server/utils/logger.js +2 -0
  23. package/dist/runtime/types/nuxt-schema.d.ts +20 -0
  24. package/dist/runtime/types/stub.d.ts +17 -0
  25. package/dist/runtime/utils/logger.d.ts +0 -0
  26. package/dist/runtime/utils/logger.js +2 -0
  27. package/package.json +3 -3
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wpnuxt/auth",
3
3
  "configKey": "wpNuxtAuth",
4
- "version": "2.0.0-alpha.0",
4
+ "version": "2.0.0-alpha.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,5 +1,99 @@
1
- import { defineNuxtModule, createResolver, addPlugin, addImports } from '@nuxt/kit';
1
+ import { existsSync, readFileSync, cpSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { defineNuxtModule, createResolver, addPlugin, addImports, addServerHandler, useLogger } from '@nuxt/kit';
2
4
 
5
+ function detectAuthCapabilities(schemaPath) {
6
+ if (!existsSync(schemaPath)) {
7
+ return null;
8
+ }
9
+ let schemaContent;
10
+ try {
11
+ schemaContent = readFileSync(schemaPath, "utf-8");
12
+ } catch {
13
+ return null;
14
+ }
15
+ if (!schemaContent || schemaContent.length < 100) {
16
+ return null;
17
+ }
18
+ const hasHeadlessLogin = /loginClients\s*[:[(]/.test(schemaContent);
19
+ const hasPasswordAuth = /\blogin\s*\(/.test(schemaContent) && /LoginInput/.test(schemaContent);
20
+ const hasRefreshToken = /refreshToken\s*\(/.test(schemaContent);
21
+ const detectedProviders = [];
22
+ const enumMatch = schemaContent.match(/enum\s+LoginProviderEnum\s*\{([^}]+)\}/);
23
+ if (enumMatch?.[1]) {
24
+ const enumBody = enumMatch[1];
25
+ const providerMatches = enumBody.match(/\b[A-Z][A-Z0-9_]+\b/g);
26
+ if (providerMatches) {
27
+ detectedProviders.push(...providerMatches);
28
+ }
29
+ }
30
+ return {
31
+ hasHeadlessLogin,
32
+ hasPasswordAuth,
33
+ hasRefreshToken,
34
+ detectedProviders
35
+ };
36
+ }
37
+ function validateAuthSchema(schemaPath, options = {}) {
38
+ const capabilities = detectAuthCapabilities(schemaPath);
39
+ if (capabilities === null) {
40
+ throw new Error(
41
+ `[WPNuxt Auth] Cannot validate GraphQL schema - file not found or unreadable.
42
+
43
+ Schema path: ${schemaPath}
44
+
45
+ Make sure @wpnuxt/core is loaded before @wpnuxt/auth in your nuxt.config.ts modules,
46
+ and that 'downloadSchema' is enabled (the default).
47
+
48
+ To fix this, run: pnpm dev:prepare`
49
+ );
50
+ }
51
+ if (!capabilities.hasHeadlessLogin) {
52
+ throw new Error(
53
+ `[WPNuxt Auth] Headless Login for WPGraphQL plugin not detected.
54
+
55
+ The @wpnuxt/auth module requires this WordPress plugin for authentication.
56
+ Your WordPress GraphQL schema is missing the required 'loginClients' query.
57
+
58
+ To fix this:
59
+ 1. Install the plugin: https://github.com/AxeWP/wp-graphql-headless-login
60
+ 2. Configure at least one authentication provider in WordPress admin
61
+ 3. Run 'pnpm dev:prepare' to refresh the GraphQL schema
62
+
63
+ To disable @wpnuxt/auth, remove it from your nuxt.config.ts modules.`
64
+ );
65
+ }
66
+ if (options.requirePassword && !capabilities.hasPasswordAuth) {
67
+ throw new Error(
68
+ `[WPNuxt Auth] Password authentication not available in GraphQL schema.
69
+
70
+ The 'login' mutation is missing from your WordPress GraphQL schema.
71
+ Make sure Headless Login for WPGraphQL is properly configured with PASSWORD provider enabled.`
72
+ );
73
+ }
74
+ if (options.requireHeadlessLogin && capabilities.detectedProviders.length === 0) {
75
+ throw new Error(
76
+ `[WPNuxt Auth] No OAuth providers detected in GraphQL schema.
77
+
78
+ Headless Login is installed but no providers are configured.
79
+ Configure OAuth providers (Google, GitHub, etc.) in WordPress admin under:
80
+ GraphQL > Headless Login > Client Settings`
81
+ );
82
+ }
83
+ }
84
+
85
+ const DEFAULT_OAUTH_CONFIG = {
86
+ enabled: false,
87
+ clientId: "",
88
+ clientSecret: "",
89
+ authorizationEndpoint: "/wp-json/moserver/authorize",
90
+ tokenEndpoint: "/wp-json/moserver/token",
91
+ userInfoEndpoint: "/wp-json/moserver/resource",
92
+ scopes: ["openid", "profile", "email"]
93
+ };
94
+ const DEFAULT_HEADLESS_LOGIN_CONFIG = {
95
+ enabled: false
96
+ };
3
97
  const module$1 = defineNuxtModule({
4
98
  meta: {
5
99
  name: "@wpnuxt/auth",
@@ -13,13 +107,48 @@ const module$1 = defineNuxtModule({
13
107
  refreshTokenMaxAge: 604800,
14
108
  redirectOnLogin: "/",
15
109
  redirectOnLogout: "/",
16
- loginPage: "/login"
110
+ loginPage: "/login",
111
+ providers: {
112
+ password: { enabled: true },
113
+ oauth: DEFAULT_OAUTH_CONFIG,
114
+ headlessLogin: DEFAULT_HEADLESS_LOGIN_CONFIG
115
+ }
17
116
  },
18
117
  async setup(options, nuxt) {
19
118
  if (!options.enabled) {
20
119
  return;
21
120
  }
22
121
  const resolver = createResolver(import.meta.url);
122
+ const baseDir = nuxt.options.srcDir || nuxt.options.rootDir;
123
+ const { resolve } = createResolver(baseDir);
124
+ const wpNuxtConfig = nuxt.options.wpNuxt;
125
+ const mergedQueriesPath = resolve(wpNuxtConfig?.queries?.mergedOutputFolder || ".queries/");
126
+ const userQueryPath = resolve(wpNuxtConfig?.queries?.extendFolder || "extend/queries/");
127
+ const authQueriesPath = resolver.resolve("./runtime/queries");
128
+ if (existsSync(authQueriesPath)) {
129
+ cpSync(authQueriesPath, mergedQueriesPath, { recursive: true });
130
+ }
131
+ if (existsSync(userQueryPath)) {
132
+ cpSync(userQueryPath, mergedQueriesPath, { recursive: true });
133
+ }
134
+ const oauthConfig = {
135
+ ...DEFAULT_OAUTH_CONFIG,
136
+ ...options.providers?.oauth
137
+ };
138
+ const headlessLoginConfig = {
139
+ ...DEFAULT_HEADLESS_LOGIN_CONFIG,
140
+ ...options.providers?.headlessLogin
141
+ };
142
+ const passwordEnabled = options.providers?.password?.enabled ?? true;
143
+ const oauthEnabled = oauthConfig.enabled && !!oauthConfig.clientId;
144
+ const headlessLoginEnabled = headlessLoginConfig.enabled ?? false;
145
+ if (passwordEnabled || headlessLoginEnabled) {
146
+ const schemaPath = join(nuxt.options.rootDir, "schema.graphql");
147
+ validateAuthSchema(schemaPath, {
148
+ requirePassword: passwordEnabled,
149
+ requireHeadlessLogin: headlessLoginEnabled
150
+ });
151
+ }
23
152
  nuxt.options.runtimeConfig.public.wpNuxtAuth = {
24
153
  cookieName: options.cookieName,
25
154
  refreshCookieName: options.refreshCookieName,
@@ -27,19 +156,80 @@ const module$1 = defineNuxtModule({
27
156
  refreshTokenMaxAge: options.refreshTokenMaxAge,
28
157
  redirectOnLogin: options.redirectOnLogin,
29
158
  redirectOnLogout: options.redirectOnLogout,
30
- loginPage: options.loginPage
159
+ loginPage: options.loginPage,
160
+ providers: {
161
+ password: { enabled: passwordEnabled },
162
+ oauth: {
163
+ enabled: oauthEnabled,
164
+ clientId: oauthConfig.clientId,
165
+ authorizationEndpoint: oauthConfig.authorizationEndpoint,
166
+ scopes: oauthConfig.scopes
167
+ },
168
+ headlessLogin: {
169
+ enabled: headlessLoginEnabled
170
+ }
171
+ }
31
172
  };
173
+ if (oauthEnabled) {
174
+ nuxt.options.runtimeConfig.wpNuxtAuthOAuth = {
175
+ clientId: oauthConfig.clientId,
176
+ clientSecret: oauthConfig.clientSecret,
177
+ tokenEndpoint: oauthConfig.tokenEndpoint,
178
+ userInfoEndpoint: oauthConfig.userInfoEndpoint
179
+ };
180
+ }
32
181
  addPlugin(resolver.resolve("./runtime/plugins/auth"));
33
182
  addImports([
34
183
  { name: "useWPAuth", from: resolver.resolve("./runtime/composables/useWPAuth") },
35
184
  { name: "useWPUser", from: resolver.resolve("./runtime/composables/useWPUser") }
36
185
  ]);
186
+ if (passwordEnabled) {
187
+ addServerHandler({
188
+ route: "/api/auth/login",
189
+ method: "post",
190
+ handler: resolver.resolve("./runtime/server/api/auth/login.post")
191
+ });
192
+ }
193
+ addServerHandler({
194
+ route: "/api/auth/logout",
195
+ method: "post",
196
+ handler: resolver.resolve("./runtime/server/api/auth/logout.post")
197
+ });
198
+ if (oauthEnabled) {
199
+ addServerHandler({
200
+ route: "/api/auth/oauth/authorize",
201
+ method: "get",
202
+ handler: resolver.resolve("./runtime/server/api/auth/oauth/authorize.get")
203
+ });
204
+ addServerHandler({
205
+ route: "/api/auth/oauth/callback",
206
+ method: "get",
207
+ handler: resolver.resolve("./runtime/server/api/auth/oauth/callback.get")
208
+ });
209
+ }
210
+ if (headlessLoginEnabled) {
211
+ addServerHandler({
212
+ route: "/api/auth/provider/:provider/authorize",
213
+ method: "get",
214
+ handler: resolver.resolve("./runtime/server/api/auth/provider/[provider]/authorize.get")
215
+ });
216
+ addServerHandler({
217
+ route: "/api/auth/provider/:provider/callback",
218
+ method: "get",
219
+ handler: resolver.resolve("./runtime/server/api/auth/provider/[provider]/callback.get")
220
+ });
221
+ }
37
222
  nuxt.hook("prepare:types", ({ references }) => {
38
223
  references.push({
39
224
  path: resolver.resolve("./runtime/types/index.ts")
40
225
  });
41
226
  });
42
- console.log("[WPNuxt Auth] Module loaded");
227
+ const logger = useLogger("wpnuxt:auth");
228
+ const providers = [];
229
+ if (passwordEnabled) providers.push("password");
230
+ if (oauthEnabled) providers.push("oauth");
231
+ if (headlessLoginEnabled) providers.push("headlessLogin");
232
+ logger.info(`Module loaded (providers: ${providers.join(", ") || "none"})`);
43
233
  }
44
234
  });
45
235
 
@@ -1,4 +1,5 @@
1
1
  import { computed, useState, useCookie, useRuntimeConfig, navigateTo, useGraphqlMutation } from "#imports";
2
+ import { logger } from "../utils/logger.js";
2
3
  export function useWPAuth() {
3
4
  const config = useRuntimeConfig().public.wpNuxtAuth;
4
5
  const authState = useState("wpnuxt-auth", () => ({
@@ -12,19 +13,29 @@ export function useWPAuth() {
12
13
  secure: process.env.NODE_ENV === "production",
13
14
  sameSite: "lax"
14
15
  });
15
- const refreshToken = useCookie(config.refreshCookieName, {
16
+ const refreshTokenCookie = useCookie(config.refreshCookieName, {
16
17
  maxAge: config.refreshTokenMaxAge,
17
18
  secure: process.env.NODE_ENV === "production",
18
19
  sameSite: "lax"
19
20
  });
21
+ const userDataCookie = useCookie("wpnuxt-user", {
22
+ maxAge: config.tokenMaxAge,
23
+ secure: process.env.NODE_ENV === "production",
24
+ sameSite: "lax"
25
+ });
20
26
  async function login(credentials) {
21
27
  authState.value.isLoading = true;
22
28
  authState.value.error = null;
23
29
  try {
24
- const { data, errors } = await useGraphqlMutation("Login", {
25
- username: credentials.username,
26
- password: credentials.password
30
+ const response = await $fetch("/api/auth/login", {
31
+ method: "POST",
32
+ body: {
33
+ username: credentials.username,
34
+ password: credentials.password
35
+ }
27
36
  });
37
+ const data = response.data;
38
+ const errors = response.errors;
28
39
  if (errors?.length) {
29
40
  const errorMessage = errors[0]?.message || "Login failed";
30
41
  authState.value.error = errorMessage;
@@ -33,7 +44,8 @@ export function useWPAuth() {
33
44
  }
34
45
  if (data?.login) {
35
46
  authToken.value = data.login.authToken;
36
- refreshToken.value = data.login.refreshToken;
47
+ refreshTokenCookie.value = data.login.refreshToken;
48
+ userDataCookie.value = JSON.stringify(data.login.user);
37
49
  authState.value.user = data.login.user;
38
50
  authState.value.isAuthenticated = true;
39
51
  authState.value.isLoading = false;
@@ -56,8 +68,11 @@ export function useWPAuth() {
56
68
  }
57
69
  }
58
70
  async function logout() {
71
+ await $fetch("/api/auth/logout", { method: "POST" }).catch(() => {
72
+ });
59
73
  authToken.value = null;
60
- refreshToken.value = null;
74
+ refreshTokenCookie.value = null;
75
+ userDataCookie.value = null;
61
76
  authState.value.user = null;
62
77
  authState.value.isAuthenticated = false;
63
78
  authState.value.error = null;
@@ -66,18 +81,19 @@ export function useWPAuth() {
66
81
  }
67
82
  }
68
83
  async function refresh() {
69
- if (!refreshToken.value) {
84
+ if (!refreshTokenCookie.value) {
70
85
  return false;
71
86
  }
72
87
  try {
73
- const { data, errors } = await useGraphqlMutation("RefreshAuthToken", {
74
- refreshToken: refreshToken.value
88
+ const { data, errors } = await useGraphqlMutation("RefreshToken", {
89
+ refreshToken: refreshTokenCookie.value
75
90
  });
76
- if (errors?.length || !data?.refreshJwtAuthToken) {
91
+ const refreshData = data;
92
+ if (errors?.length || !refreshData?.refreshToken?.success) {
77
93
  await logout();
78
94
  return false;
79
95
  }
80
- authToken.value = data.refreshJwtAuthToken.authToken;
96
+ authToken.value = refreshData.refreshToken.authToken;
81
97
  return true;
82
98
  } catch {
83
99
  await logout();
@@ -87,6 +103,84 @@ export function useWPAuth() {
87
103
  function getToken() {
88
104
  return authToken.value || null;
89
105
  }
106
+ function getProviders() {
107
+ const providers = config.providers;
108
+ return {
109
+ password: providers?.password?.enabled ?? true,
110
+ oauth: providers?.oauth?.enabled ?? false,
111
+ headlessLogin: providers?.headlessLogin?.enabled ?? false
112
+ };
113
+ }
114
+ async function loginWithOAuth() {
115
+ const providers = getProviders();
116
+ if (!providers.oauth) {
117
+ logger.warn("OAuth is not enabled");
118
+ return;
119
+ }
120
+ await navigateTo("/api/auth/oauth/authorize", { external: true });
121
+ }
122
+ function hasPasswordAuth() {
123
+ return getProviders().password;
124
+ }
125
+ function hasOAuthAuth() {
126
+ return getProviders().oauth;
127
+ }
128
+ function hasHeadlessLoginAuth() {
129
+ return getProviders().headlessLogin;
130
+ }
131
+ async function fetchHeadlessLoginProviders() {
132
+ if (!hasHeadlessLoginAuth()) {
133
+ return [];
134
+ }
135
+ try {
136
+ const runtimeConfig = useRuntimeConfig();
137
+ const wordpressUrl = runtimeConfig.public.wordpressUrl;
138
+ const graphqlEndpoint = runtimeConfig.public.wpNuxt?.graphqlEndpoint || "/graphql";
139
+ if (!wordpressUrl) {
140
+ logger.warn("WordPress URL not configured");
141
+ return [];
142
+ }
143
+ const response = await $fetch(`${wordpressUrl}${graphqlEndpoint}`, {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: {
147
+ query: `
148
+ query LoginClients {
149
+ loginClients {
150
+ name
151
+ provider
152
+ authorizationUrl
153
+ isEnabled
154
+ }
155
+ }
156
+ `
157
+ }
158
+ });
159
+ if (response.errors?.length) {
160
+ logger.warn("Failed to fetch login clients:", response.errors[0]?.message);
161
+ return [];
162
+ }
163
+ const clients = response.data?.loginClients || [];
164
+ return clients.filter(
165
+ (client) => client.isEnabled && client.provider !== "PASSWORD" && client.provider !== "SITETOKEN" && client.authorizationUrl
166
+ ).map((client) => ({
167
+ name: client.name,
168
+ provider: client.provider,
169
+ authorizationUrl: client.authorizationUrl,
170
+ isEnabled: client.isEnabled
171
+ }));
172
+ } catch (error) {
173
+ logger.warn("Failed to fetch login providers:", error);
174
+ return [];
175
+ }
176
+ }
177
+ async function loginWithProvider(provider) {
178
+ if (!hasHeadlessLoginAuth()) {
179
+ logger.warn("Headless Login is not enabled");
180
+ return;
181
+ }
182
+ await navigateTo(`/api/auth/provider/${provider.toLowerCase()}/authorize`, { external: true });
183
+ }
90
184
  return {
91
185
  // State
92
186
  state: authState,
@@ -96,8 +190,15 @@ export function useWPAuth() {
96
190
  error: computed(() => authState.value.error),
97
191
  // Methods
98
192
  login,
193
+ loginWithOAuth,
194
+ loginWithProvider,
99
195
  logout,
100
196
  refresh,
101
- getToken
197
+ getToken,
198
+ getProviders,
199
+ hasPasswordAuth,
200
+ hasOAuthAuth,
201
+ hasHeadlessLoginAuth,
202
+ fetchHeadlessLoginProviders
102
203
  };
103
204
  }
@@ -8,12 +8,13 @@ export function useWPUser() {
8
8
  errorState.value = null;
9
9
  try {
10
10
  const { data, errors } = await useGraphqlQuery("Viewer");
11
+ const viewerData = data;
11
12
  if (errors?.length) {
12
13
  errorState.value = errors[0]?.message || "Failed to fetch user";
13
14
  loadingState.value = false;
14
15
  return null;
15
16
  }
16
- userState.value = data?.viewer || null;
17
+ userState.value = viewerData?.viewer || null;
17
18
  loadingState.value = false;
18
19
  return userState.value;
19
20
  } catch (error) {
@@ -11,8 +11,18 @@ export default defineNuxtPlugin({
11
11
  error: null
12
12
  }));
13
13
  const authToken = useCookie(config.cookieName);
14
+ const headlessLoginUserCookie = useCookie("wpnuxt-user");
15
+ const oauthUserCookie = useCookie("wpnuxt-oauth-user");
14
16
  if (authToken.value) {
15
17
  authState.value.isAuthenticated = true;
18
+ const userCookieValue = headlessLoginUserCookie.value || oauthUserCookie.value;
19
+ if (userCookieValue) {
20
+ try {
21
+ const userData = typeof userCookieValue === "string" ? JSON.parse(userCookieValue) : userCookieValue;
22
+ authState.value.user = userData;
23
+ } catch {
24
+ }
25
+ }
16
26
  }
17
27
  authState.value.isLoading = false;
18
28
  }
@@ -1,8 +1,17 @@
1
- # Login mutation for WPGraphQL JWT Authentication
1
+ # Login with username and password
2
+ # Requires: Headless Login for WPGraphQL plugin
2
3
  mutation Login($username: String!, $password: String!) {
3
- login(input: { username: $username, password: $password }) {
4
+ login(input: {
5
+ provider: PASSWORD
6
+ credentials: {
7
+ username: $username
8
+ password: $password
9
+ }
10
+ }) {
4
11
  authToken
12
+ authTokenExpiration
5
13
  refreshToken
14
+ refreshTokenExpiration
6
15
  user {
7
16
  id
8
17
  databaseId
@@ -23,9 +32,106 @@ mutation Login($username: String!, $password: String!) {
23
32
  }
24
33
  }
25
34
 
26
- # Refresh auth token mutation
27
- mutation RefreshAuthToken($refreshToken: String!) {
28
- refreshJwtAuthToken(input: { jwtRefreshToken: $refreshToken }) {
35
+ # Refresh authentication token
36
+ mutation RefreshToken($refreshToken: String!) {
37
+ refreshToken(input: { refreshToken: $refreshToken }) {
29
38
  authToken
39
+ authTokenExpiration
40
+ success
41
+ }
42
+ }
43
+
44
+ # Register a new user (requires user registration to be enabled in WordPress)
45
+ mutation RegisterUser($username: String!, $email: String!, $password: String!) {
46
+ registerUser(input: { username: $username, email: $email, password: $password }) {
47
+ user {
48
+ id
49
+ databaseId
50
+ username
51
+ email
52
+ name
53
+ }
54
+ }
55
+ }
56
+
57
+ # Update the current user's profile
58
+ mutation UpdateUser($id: ID!, $firstName: String, $lastName: String, $nickname: String, $description: String) {
59
+ updateUser(input: { id: $id, firstName: $firstName, lastName: $lastName, nickname: $nickname, description: $description }) {
60
+ user {
61
+ id
62
+ databaseId
63
+ username
64
+ email
65
+ firstName
66
+ lastName
67
+ nickname
68
+ name
69
+ description
70
+ }
71
+ }
72
+ }
73
+
74
+ # Send password reset email
75
+ mutation SendPasswordResetEmail($username: String!) {
76
+ sendPasswordResetEmail(input: { username: $username }) {
77
+ success
78
+ }
79
+ }
80
+
81
+ # Reset password with key from email
82
+ mutation ResetUserPassword($key: String!, $login: String!, $password: String!) {
83
+ resetUserPassword(input: { key: $key, login: $login, password: $password }) {
84
+ user {
85
+ id
86
+ databaseId
87
+ username
88
+ email
89
+ }
90
+ }
91
+ }
92
+
93
+ # Get available login clients/providers from Headless Login for WPGraphQL
94
+ query LoginClients {
95
+ loginClients {
96
+ name
97
+ provider
98
+ authorizationUrl
99
+ isEnabled
100
+ }
101
+ }
102
+
103
+ # Login with an OAuth provider (Google, GitHub, etc.) using authorization code
104
+ # Requires: Headless Login for WPGraphQL plugin with provider configured
105
+ mutation LoginWithProvider($provider: LoginProviderEnum!, $code: String!, $state: String) {
106
+ login(input: {
107
+ provider: $provider
108
+ oauthResponse: {
109
+ code: $code
110
+ state: $state
111
+ }
112
+ }) {
113
+ authToken
114
+ authTokenExpiration
115
+ refreshToken
116
+ refreshTokenExpiration
117
+ user {
118
+ id
119
+ databaseId
120
+ name
121
+ email
122
+ firstName
123
+ lastName
124
+ username
125
+ nickname
126
+ description
127
+ avatar {
128
+ url
129
+ }
130
+ roles {
131
+ nodes {
132
+ name
133
+ }
134
+ }
135
+ }
30
136
  }
31
137
  }
@@ -0,0 +1,26 @@
1
+ query Viewer {
2
+ viewer {
3
+ id
4
+ databaseId
5
+ userId
6
+ username
7
+ email
8
+ firstName
9
+ lastName
10
+ name
11
+ nickname
12
+ description
13
+ locale
14
+ url
15
+ uri
16
+ avatar {
17
+ url
18
+ }
19
+ roles {
20
+ nodes {
21
+ name
22
+ }
23
+ }
24
+ capabilities
25
+ }
26
+ }
@@ -0,0 +1,11 @@
1
+ fragment GeneralSettings on GeneralSettings {
2
+ title
3
+ description
4
+ url
5
+ dateFormat
6
+ language
7
+ startOfWeek
8
+ timezone
9
+ timeFormat
10
+ email
11
+ }
File without changes
@@ -0,0 +1,51 @@
1
+ import { defineEventHandler, readBody, createError } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ const LOGIN_MUTATION = `
4
+ mutation Login($username: String!, $password: String!) {
5
+ login(input: {
6
+ provider: PASSWORD
7
+ credentials: {
8
+ username: $username
9
+ password: $password
10
+ }
11
+ }) {
12
+ authToken
13
+ authTokenExpiration
14
+ refreshToken
15
+ refreshTokenExpiration
16
+ user {
17
+ id
18
+ databaseId
19
+ name
20
+ email
21
+ firstName
22
+ lastName
23
+ username
24
+ avatar { url }
25
+ roles { nodes { name } }
26
+ }
27
+ }
28
+ }
29
+ `;
30
+ export default defineEventHandler(async (event) => {
31
+ const config = useRuntimeConfig();
32
+ const body = await readBody(event);
33
+ if (!body?.username || !body?.password) {
34
+ throw createError({
35
+ statusCode: 400,
36
+ message: "Username and password are required"
37
+ });
38
+ }
39
+ const response = await $fetch(config.graphqlMiddleware.graphqlEndpoint, {
40
+ method: "POST",
41
+ body: {
42
+ query: LOGIN_MUTATION,
43
+ variables: {
44
+ username: body.username,
45
+ password: body.password
46
+ },
47
+ operationName: "Login"
48
+ }
49
+ });
50
+ return response;
51
+ });
File without changes
@@ -0,0 +1,18 @@
1
+ import { defineEventHandler, deleteCookie } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ export default defineEventHandler((event) => {
4
+ const config = useRuntimeConfig().public.wpNuxtAuth;
5
+ deleteCookie(event, config.cookieName, {
6
+ path: "/"
7
+ });
8
+ deleteCookie(event, config.refreshCookieName, {
9
+ path: "/"
10
+ });
11
+ deleteCookie(event, "wpnuxt-oauth-user", {
12
+ path: "/"
13
+ });
14
+ deleteCookie(event, "wpnuxt-oauth-state", {
15
+ path: "/"
16
+ });
17
+ return { success: true };
18
+ });
@@ -0,0 +1,36 @@
1
+ import { defineEventHandler, setCookie, getRequestURL, setResponseStatus, setResponseHeader } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ export default defineEventHandler(async (event) => {
4
+ const config = useRuntimeConfig();
5
+ const publicConfig = config.public.wpNuxtAuth;
6
+ const wordpressUrl = config.public.wordpressUrl;
7
+ if (!wordpressUrl) {
8
+ throw new Error("WordPress URL not configured");
9
+ }
10
+ if (!publicConfig.providers.oauth.enabled) {
11
+ throw new Error("OAuth authentication is not enabled");
12
+ }
13
+ const state = crypto.randomUUID();
14
+ setCookie(event, "wpnuxt-oauth-state", state, {
15
+ httpOnly: true,
16
+ secure: process.env.NODE_ENV === "production",
17
+ sameSite: "lax",
18
+ maxAge: 600,
19
+ // 10 minutes
20
+ path: "/"
21
+ });
22
+ const requestUrl = getRequestURL(event);
23
+ const callbackUrl = `${requestUrl.origin}/api/auth/oauth/callback`;
24
+ const authUrl = new URL(
25
+ publicConfig.providers.oauth.authorizationEndpoint,
26
+ wordpressUrl
27
+ );
28
+ authUrl.searchParams.set("response_type", "code");
29
+ authUrl.searchParams.set("client_id", publicConfig.providers.oauth.clientId);
30
+ authUrl.searchParams.set("redirect_uri", callbackUrl);
31
+ authUrl.searchParams.set("state", state);
32
+ authUrl.searchParams.set("scope", publicConfig.providers.oauth.scopes.join(" "));
33
+ setResponseStatus(event, 302);
34
+ setResponseHeader(event, "Location", authUrl.toString());
35
+ return "";
36
+ });
@@ -0,0 +1,106 @@
1
+ import { defineEventHandler, getQuery, getCookie, setCookie, deleteCookie, sendRedirect, createError, getRequestURL } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { logger } from "../../../utils/logger.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const query = getQuery(event);
6
+ const config = useRuntimeConfig();
7
+ const publicConfig = config.public.wpNuxtAuth;
8
+ const privateConfig = config.wpNuxtAuthOAuth;
9
+ const wordpressUrl = config.public.wordpressUrl;
10
+ if (!wordpressUrl) {
11
+ throw createError({ statusCode: 500, message: "WordPress URL not configured" });
12
+ }
13
+ if (!privateConfig) {
14
+ throw createError({ statusCode: 500, message: "OAuth not configured" });
15
+ }
16
+ if (query.error) {
17
+ const errorDesc = query.error_description || query.error;
18
+ throw createError({ statusCode: 400, message: `OAuth error: ${errorDesc}` });
19
+ }
20
+ const code = query.code;
21
+ if (!code) {
22
+ throw createError({ statusCode: 400, message: "Missing authorization code" });
23
+ }
24
+ const state = query.state;
25
+ const storedState = getCookie(event, "wpnuxt-oauth-state");
26
+ if (!state || state !== storedState) {
27
+ throw createError({ statusCode: 400, message: "Invalid state parameter" });
28
+ }
29
+ deleteCookie(event, "wpnuxt-oauth-state");
30
+ const requestUrl = getRequestURL(event);
31
+ const callbackUrl = `${requestUrl.origin}/api/auth/oauth/callback`;
32
+ const tokenUrl = new URL(privateConfig.tokenEndpoint, wordpressUrl);
33
+ const tokenResponse = await $fetch(tokenUrl.toString(), {
34
+ method: "POST",
35
+ headers: {
36
+ "Content-Type": "application/x-www-form-urlencoded"
37
+ },
38
+ body: new URLSearchParams({
39
+ grant_type: "authorization_code",
40
+ code,
41
+ redirect_uri: callbackUrl,
42
+ client_id: privateConfig.clientId,
43
+ client_secret: privateConfig.clientSecret
44
+ }).toString()
45
+ }).catch((error) => {
46
+ logger.error("Token exchange failed:", error);
47
+ throw createError({ statusCode: 500, message: "Failed to exchange authorization code" });
48
+ });
49
+ if (!tokenResponse.access_token) {
50
+ throw createError({ statusCode: 500, message: "No access token received" });
51
+ }
52
+ const userInfoUrl = new URL(privateConfig.userInfoEndpoint, wordpressUrl);
53
+ const userInfo = await $fetch(userInfoUrl.toString(), {
54
+ headers: {
55
+ Authorization: `Bearer ${tokenResponse.access_token}`
56
+ }
57
+ }).catch((error) => {
58
+ logger.error("User info fetch failed:", error);
59
+ return null;
60
+ });
61
+ const isProduction = process.env.NODE_ENV === "production";
62
+ setCookie(event, publicConfig.cookieName, tokenResponse.access_token, {
63
+ httpOnly: true,
64
+ secure: isProduction,
65
+ sameSite: "lax",
66
+ maxAge: tokenResponse.expires_in || publicConfig.tokenMaxAge,
67
+ path: "/"
68
+ });
69
+ if (tokenResponse.refresh_token) {
70
+ setCookie(event, publicConfig.refreshCookieName, tokenResponse.refresh_token, {
71
+ httpOnly: true,
72
+ secure: isProduction,
73
+ sameSite: "lax",
74
+ maxAge: publicConfig.refreshTokenMaxAge,
75
+ path: "/"
76
+ });
77
+ }
78
+ if (userInfo) {
79
+ const userId = userInfo.sub || userInfo.id || userInfo.ID;
80
+ const firstName = userInfo.given_name || userInfo.first_name || "";
81
+ const lastName = userInfo.family_name || userInfo.last_name || "";
82
+ const displayName = userInfo.name || userInfo.display_name || `${firstName} ${lastName}`.trim();
83
+ const username = userInfo.preferred_username || userInfo.username || userInfo.user_login || userInfo.nickname;
84
+ const avatarUrl = userInfo.picture || userInfo.avatar_url;
85
+ const userData = {
86
+ id: userId?.toString() || "",
87
+ databaseId: typeof userId === "number" ? userId : Number.parseInt(userId?.toString() || "0", 10),
88
+ email: userInfo.email,
89
+ name: displayName,
90
+ firstName,
91
+ lastName,
92
+ username,
93
+ nickname: userInfo.nickname,
94
+ avatar: avatarUrl ? { url: avatarUrl } : void 0
95
+ };
96
+ setCookie(event, "wpnuxt-oauth-user", JSON.stringify(userData), {
97
+ httpOnly: false,
98
+ // Client needs to read this
99
+ secure: isProduction,
100
+ sameSite: "lax",
101
+ maxAge: tokenResponse.expires_in || publicConfig.tokenMaxAge,
102
+ path: "/"
103
+ });
104
+ }
105
+ return sendRedirect(event, publicConfig.redirectOnLogin);
106
+ });
@@ -0,0 +1,82 @@
1
+ import { defineEventHandler, getRouterParam, setCookie, sendRedirect, createError, getRequestURL } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { logger } from "../../../../utils/logger.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const provider = getRouterParam(event, "provider");
6
+ if (!provider) {
7
+ throw createError({ statusCode: 400, message: "Missing provider parameter" });
8
+ }
9
+ const config = useRuntimeConfig();
10
+ const wordpressUrl = config.public.wordpressUrl;
11
+ const wpNuxtConfig = config.public.wpNuxt;
12
+ const graphqlEndpoint = wpNuxtConfig?.graphqlEndpoint || "/graphql";
13
+ if (!wordpressUrl) {
14
+ throw createError({ statusCode: 500, message: "WordPress URL not configured" });
15
+ }
16
+ const graphqlUrl = `${wordpressUrl}${graphqlEndpoint}`;
17
+ const response = await $fetch(graphqlUrl, {
18
+ method: "POST",
19
+ headers: {
20
+ "Content-Type": "application/json"
21
+ },
22
+ body: {
23
+ query: `
24
+ query LoginClients {
25
+ loginClients {
26
+ name
27
+ provider
28
+ authorizationUrl
29
+ isEnabled
30
+ }
31
+ }
32
+ `
33
+ }
34
+ }).catch((error) => {
35
+ logger.error("Failed to fetch login clients:", error);
36
+ throw createError({ statusCode: 500, message: "Failed to fetch login providers from WordPress" });
37
+ });
38
+ if (response.errors?.length) {
39
+ logger.error("GraphQL errors:", response.errors);
40
+ throw createError({ statusCode: 500, message: response.errors[0]?.message || "GraphQL error" });
41
+ }
42
+ const loginClients = response.data?.loginClients || [];
43
+ const providerUpperCase = provider.toUpperCase();
44
+ const loginClient = loginClients.find(
45
+ (client) => client.provider === providerUpperCase && client.isEnabled
46
+ );
47
+ if (!loginClient) {
48
+ throw createError({
49
+ statusCode: 404,
50
+ message: `Provider '${provider}' not found or not enabled. Available providers: ${loginClients.filter((c) => c.isEnabled).map((c) => c.name).join(", ")}`
51
+ });
52
+ }
53
+ if (!loginClient.authorizationUrl) {
54
+ throw createError({
55
+ statusCode: 500,
56
+ message: `Provider '${provider}' does not have an authorization URL configured`
57
+ });
58
+ }
59
+ const state = crypto.randomUUID();
60
+ const isProduction = process.env.NODE_ENV === "production";
61
+ setCookie(event, "wpnuxt-headless-login-state", state, {
62
+ httpOnly: true,
63
+ secure: isProduction,
64
+ sameSite: "lax",
65
+ maxAge: 600,
66
+ // 10 minutes
67
+ path: "/"
68
+ });
69
+ setCookie(event, "wpnuxt-headless-login-provider", providerUpperCase, {
70
+ httpOnly: true,
71
+ secure: isProduction,
72
+ sameSite: "lax",
73
+ maxAge: 600,
74
+ path: "/"
75
+ });
76
+ const authUrl = new URL(loginClient.authorizationUrl);
77
+ authUrl.searchParams.set("state", state);
78
+ const requestUrl = getRequestURL(event);
79
+ const callbackUrl = `${requestUrl.origin}/api/auth/provider/${provider}/callback`;
80
+ authUrl.searchParams.set("redirect_uri", callbackUrl);
81
+ return sendRedirect(event, authUrl.toString());
82
+ });
@@ -0,0 +1,136 @@
1
+ import { defineEventHandler, getQuery, getRouterParam, getCookie, setCookie, deleteCookie, sendRedirect, createError } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { logger } from "../../../../utils/logger.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const query = getQuery(event);
6
+ const routeProvider = getRouterParam(event, "provider");
7
+ const config = useRuntimeConfig();
8
+ const publicConfig = config.public.wpNuxtAuth;
9
+ const wordpressUrl = config.public.wordpressUrl;
10
+ const wpNuxtConfig = config.public.wpNuxt;
11
+ const graphqlEndpoint = wpNuxtConfig?.graphqlEndpoint || "/graphql";
12
+ if (!wordpressUrl) {
13
+ throw createError({ statusCode: 500, message: "WordPress URL not configured" });
14
+ }
15
+ if (query.error) {
16
+ const errorDesc = query.error_description || query.error;
17
+ throw createError({ statusCode: 400, message: `OAuth error: ${errorDesc}` });
18
+ }
19
+ const code = query.code;
20
+ if (!code) {
21
+ throw createError({ statusCode: 400, message: "Missing authorization code" });
22
+ }
23
+ const state = query.state;
24
+ const storedState = getCookie(event, "wpnuxt-headless-login-state");
25
+ if (!state || state !== storedState) {
26
+ throw createError({ statusCode: 400, message: "Invalid state parameter" });
27
+ }
28
+ const storedProvider = getCookie(event, "wpnuxt-headless-login-provider");
29
+ const provider = storedProvider || routeProvider?.toUpperCase();
30
+ if (!provider) {
31
+ throw createError({ statusCode: 400, message: "Missing provider information" });
32
+ }
33
+ deleteCookie(event, "wpnuxt-headless-login-state");
34
+ deleteCookie(event, "wpnuxt-headless-login-provider");
35
+ const graphqlUrl = `${wordpressUrl}${graphqlEndpoint}`;
36
+ const response = await $fetch(graphqlUrl, {
37
+ method: "POST",
38
+ headers: {
39
+ "Content-Type": "application/json"
40
+ },
41
+ body: {
42
+ query: `
43
+ mutation LoginWithProvider($provider: LoginProviderEnum!, $code: String!, $state: String) {
44
+ login(input: {
45
+ provider: $provider
46
+ oauthResponse: {
47
+ code: $code
48
+ state: $state
49
+ }
50
+ }) {
51
+ authToken
52
+ authTokenExpiration
53
+ refreshToken
54
+ refreshTokenExpiration
55
+ user {
56
+ id
57
+ databaseId
58
+ name
59
+ email
60
+ firstName
61
+ lastName
62
+ username
63
+ nickname
64
+ description
65
+ avatar {
66
+ url
67
+ }
68
+ roles {
69
+ nodes {
70
+ name
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ `,
77
+ variables: {
78
+ provider,
79
+ code,
80
+ state
81
+ }
82
+ }
83
+ }).catch((error) => {
84
+ logger.error("Login mutation failed:", error);
85
+ throw createError({ statusCode: 500, message: "Failed to authenticate with WordPress" });
86
+ });
87
+ if (response.errors?.length) {
88
+ logger.error("GraphQL errors:", response.errors);
89
+ throw createError({ statusCode: 400, message: response.errors[0]?.message || "Authentication failed" });
90
+ }
91
+ const loginData = response.data?.login;
92
+ if (!loginData?.authToken) {
93
+ throw createError({ statusCode: 500, message: "No auth token received from WordPress" });
94
+ }
95
+ const isProduction = process.env.NODE_ENV === "production";
96
+ setCookie(event, publicConfig.cookieName, loginData.authToken, {
97
+ httpOnly: true,
98
+ secure: isProduction,
99
+ sameSite: "lax",
100
+ maxAge: publicConfig.tokenMaxAge,
101
+ path: "/"
102
+ });
103
+ if (loginData.refreshToken) {
104
+ setCookie(event, publicConfig.refreshCookieName, loginData.refreshToken, {
105
+ httpOnly: true,
106
+ secure: isProduction,
107
+ sameSite: "lax",
108
+ maxAge: publicConfig.refreshTokenMaxAge,
109
+ path: "/"
110
+ });
111
+ }
112
+ if (loginData.user) {
113
+ const userData = {
114
+ id: loginData.user.id,
115
+ databaseId: loginData.user.databaseId,
116
+ email: loginData.user.email,
117
+ name: loginData.user.name,
118
+ firstName: loginData.user.firstName,
119
+ lastName: loginData.user.lastName,
120
+ username: loginData.user.username,
121
+ nickname: loginData.user.nickname,
122
+ description: loginData.user.description,
123
+ avatar: loginData.user.avatar,
124
+ roles: loginData.user.roles
125
+ };
126
+ setCookie(event, "wpnuxt-user", JSON.stringify(userData), {
127
+ httpOnly: false,
128
+ // Client needs to read this for hydration
129
+ secure: isProduction,
130
+ sameSite: "lax",
131
+ maxAge: publicConfig.tokenMaxAge,
132
+ path: "/"
133
+ });
134
+ }
135
+ return sendRedirect(event, publicConfig.redirectOnLogin);
136
+ });
File without changes
@@ -0,0 +1,2 @@
1
+ import { consola } from "consola";
2
+ export const logger = consola.withTag("wpnuxt:auth");
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Stub for nuxt/schema types
3
+ * Used during module development when Nuxt types aren't available
4
+ */
5
+
6
+ /* eslint-disable @typescript-eslint/no-explicit-any */
7
+
8
+ export interface PublicRuntimeConfig {
9
+ [key: string]: any
10
+ }
11
+
12
+ export interface RuntimeConfig {
13
+ public: PublicRuntimeConfig
14
+ [key: string]: any
15
+ }
16
+
17
+ export interface NuxtConfig {
18
+ runtimeConfig?: RuntimeConfig
19
+ [key: string]: any
20
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Type stubs for Nuxt-generated imports.
3
+ * These are used during module development when .nuxt folder doesn't exist.
4
+ * Actual types are generated at runtime in consuming applications.
5
+ */
6
+
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+
9
+ // Stub for #imports - using generic functions to allow type arguments
10
+ export function computed<T>(getter: () => T): { value: T }
11
+ export function defineNuxtPlugin(plugin: any): any
12
+ export function navigateTo(to: any, options?: any): any
13
+ export function useCookie<T = any>(name: string, options?: any): { value: T | null }
14
+ export function useGraphqlMutation<_T = any>(name: string, options?: any): any
15
+ export function useGraphqlQuery<_T = any>(name: string, options?: any): any
16
+ export function useRuntimeConfig(): any
17
+ export function useState<T = any>(key: string, init?: () => T): { value: T }
File without changes
@@ -0,0 +1,2 @@
1
+ import { consola } from "consola";
2
+ export const logger = consola.withTag("wpnuxt:auth");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpnuxt/auth",
3
- "version": "2.0.0-alpha.1",
3
+ "version": "2.0.0-alpha.3",
4
4
  "description": "Authentication module for WPNuxt using WPGraphQL JWT",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -45,11 +45,11 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "nuxt": "^4.0.0",
48
- "@wpnuxt/core": "2.0.0-alpha.1"
48
+ "@wpnuxt/core": "2.0.0-alpha.3"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "nuxt-module-build build",
52
- "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare",
52
+ "dev:prepare": "nuxt-module-build build --stub",
53
53
  "typecheck": "vue-tsc --noEmit",
54
54
  "clean": "rm -rf dist .nuxt node_modules"
55
55
  }