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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1",
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 } 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,79 @@ 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 providers = [];
228
+ if (passwordEnabled) providers.push("password");
229
+ if (oauthEnabled) providers.push("oauth");
230
+ if (headlessLoginEnabled) providers.push("headlessLogin");
231
+ console.log(`[WPNuxt Auth] Module loaded (providers: ${providers.join(", ") || "none"})`);
43
232
  }
44
233
  });
45
234
 
@@ -12,19 +12,29 @@ export function useWPAuth() {
12
12
  secure: process.env.NODE_ENV === "production",
13
13
  sameSite: "lax"
14
14
  });
15
- const refreshToken = useCookie(config.refreshCookieName, {
15
+ const refreshTokenCookie = useCookie(config.refreshCookieName, {
16
16
  maxAge: config.refreshTokenMaxAge,
17
17
  secure: process.env.NODE_ENV === "production",
18
18
  sameSite: "lax"
19
19
  });
20
+ const userDataCookie = useCookie("wpnuxt-user", {
21
+ maxAge: config.tokenMaxAge,
22
+ secure: process.env.NODE_ENV === "production",
23
+ sameSite: "lax"
24
+ });
20
25
  async function login(credentials) {
21
26
  authState.value.isLoading = true;
22
27
  authState.value.error = null;
23
28
  try {
24
- const { data, errors } = await useGraphqlMutation("Login", {
25
- username: credentials.username,
26
- password: credentials.password
29
+ const response = await $fetch("/api/auth/login", {
30
+ method: "POST",
31
+ body: {
32
+ username: credentials.username,
33
+ password: credentials.password
34
+ }
27
35
  });
36
+ const data = response.data;
37
+ const errors = response.errors;
28
38
  if (errors?.length) {
29
39
  const errorMessage = errors[0]?.message || "Login failed";
30
40
  authState.value.error = errorMessage;
@@ -33,7 +43,8 @@ export function useWPAuth() {
33
43
  }
34
44
  if (data?.login) {
35
45
  authToken.value = data.login.authToken;
36
- refreshToken.value = data.login.refreshToken;
46
+ refreshTokenCookie.value = data.login.refreshToken;
47
+ userDataCookie.value = JSON.stringify(data.login.user);
37
48
  authState.value.user = data.login.user;
38
49
  authState.value.isAuthenticated = true;
39
50
  authState.value.isLoading = false;
@@ -56,8 +67,11 @@ export function useWPAuth() {
56
67
  }
57
68
  }
58
69
  async function logout() {
70
+ await $fetch("/api/auth/logout", { method: "POST" }).catch(() => {
71
+ });
59
72
  authToken.value = null;
60
- refreshToken.value = null;
73
+ refreshTokenCookie.value = null;
74
+ userDataCookie.value = null;
61
75
  authState.value.user = null;
62
76
  authState.value.isAuthenticated = false;
63
77
  authState.value.error = null;
@@ -66,18 +80,19 @@ export function useWPAuth() {
66
80
  }
67
81
  }
68
82
  async function refresh() {
69
- if (!refreshToken.value) {
83
+ if (!refreshTokenCookie.value) {
70
84
  return false;
71
85
  }
72
86
  try {
73
- const { data, errors } = await useGraphqlMutation("RefreshAuthToken", {
74
- refreshToken: refreshToken.value
87
+ const { data, errors } = await useGraphqlMutation("RefreshToken", {
88
+ refreshToken: refreshTokenCookie.value
75
89
  });
76
- if (errors?.length || !data?.refreshJwtAuthToken) {
90
+ const refreshData = data;
91
+ if (errors?.length || !refreshData?.refreshToken?.success) {
77
92
  await logout();
78
93
  return false;
79
94
  }
80
- authToken.value = data.refreshJwtAuthToken.authToken;
95
+ authToken.value = refreshData.refreshToken.authToken;
81
96
  return true;
82
97
  } catch {
83
98
  await logout();
@@ -87,6 +102,84 @@ export function useWPAuth() {
87
102
  function getToken() {
88
103
  return authToken.value || null;
89
104
  }
105
+ function getProviders() {
106
+ const providers = config.providers;
107
+ return {
108
+ password: providers?.password?.enabled ?? true,
109
+ oauth: providers?.oauth?.enabled ?? false,
110
+ headlessLogin: providers?.headlessLogin?.enabled ?? false
111
+ };
112
+ }
113
+ async function loginWithOAuth() {
114
+ const providers = getProviders();
115
+ if (!providers.oauth) {
116
+ console.warn("[WPNuxt Auth] OAuth is not enabled");
117
+ return;
118
+ }
119
+ await navigateTo("/api/auth/oauth/authorize", { external: true });
120
+ }
121
+ function hasPasswordAuth() {
122
+ return getProviders().password;
123
+ }
124
+ function hasOAuthAuth() {
125
+ return getProviders().oauth;
126
+ }
127
+ function hasHeadlessLoginAuth() {
128
+ return getProviders().headlessLogin;
129
+ }
130
+ async function fetchHeadlessLoginProviders() {
131
+ if (!hasHeadlessLoginAuth()) {
132
+ return [];
133
+ }
134
+ try {
135
+ const runtimeConfig = useRuntimeConfig();
136
+ const wordpressUrl = runtimeConfig.public.wordpressUrl;
137
+ const graphqlEndpoint = runtimeConfig.public.wpNuxt?.graphqlEndpoint || "/graphql";
138
+ if (!wordpressUrl) {
139
+ console.warn("[WPNuxt Auth] WordPress URL not configured");
140
+ return [];
141
+ }
142
+ const response = await $fetch(`${wordpressUrl}${graphqlEndpoint}`, {
143
+ method: "POST",
144
+ headers: { "Content-Type": "application/json" },
145
+ body: {
146
+ query: `
147
+ query LoginClients {
148
+ loginClients {
149
+ name
150
+ provider
151
+ authorizationUrl
152
+ isEnabled
153
+ }
154
+ }
155
+ `
156
+ }
157
+ });
158
+ if (response.errors?.length) {
159
+ console.warn("[WPNuxt Auth] Failed to fetch login clients:", response.errors[0]?.message);
160
+ return [];
161
+ }
162
+ const clients = response.data?.loginClients || [];
163
+ return clients.filter(
164
+ (client) => client.isEnabled && client.provider !== "PASSWORD" && client.provider !== "SITETOKEN" && client.authorizationUrl
165
+ ).map((client) => ({
166
+ name: client.name,
167
+ provider: client.provider,
168
+ authorizationUrl: client.authorizationUrl,
169
+ isEnabled: client.isEnabled
170
+ }));
171
+ } catch (error) {
172
+ console.warn("[WPNuxt Auth] Failed to fetch login providers:", error);
173
+ return [];
174
+ }
175
+ }
176
+ async function loginWithProvider(provider) {
177
+ if (!hasHeadlessLoginAuth()) {
178
+ console.warn("[WPNuxt Auth] Headless Login is not enabled");
179
+ return;
180
+ }
181
+ await navigateTo(`/api/auth/provider/${provider.toLowerCase()}/authorize`, { external: true });
182
+ }
90
183
  return {
91
184
  // State
92
185
  state: authState,
@@ -96,8 +189,15 @@ export function useWPAuth() {
96
189
  error: computed(() => authState.value.error),
97
190
  // Methods
98
191
  login,
192
+ loginWithOAuth,
193
+ loginWithProvider,
99
194
  logout,
100
195
  refresh,
101
- getToken
196
+ getToken,
197
+ getProviders,
198
+ hasPasswordAuth,
199
+ hasOAuthAuth,
200
+ hasHeadlessLoginAuth,
201
+ fetchHeadlessLoginProviders
102
202
  };
103
203
  }
@@ -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,105 @@
1
+ import { defineEventHandler, getQuery, getCookie, setCookie, deleteCookie, sendRedirect, createError, getRequestURL } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ export default defineEventHandler(async (event) => {
4
+ const query = getQuery(event);
5
+ const config = useRuntimeConfig();
6
+ const publicConfig = config.public.wpNuxtAuth;
7
+ const privateConfig = config.wpNuxtAuthOAuth;
8
+ const wordpressUrl = config.public.wordpressUrl;
9
+ if (!wordpressUrl) {
10
+ throw createError({ statusCode: 500, message: "WordPress URL not configured" });
11
+ }
12
+ if (!privateConfig) {
13
+ throw createError({ statusCode: 500, message: "OAuth 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-oauth-state");
25
+ if (!state || state !== storedState) {
26
+ throw createError({ statusCode: 400, message: "Invalid state parameter" });
27
+ }
28
+ deleteCookie(event, "wpnuxt-oauth-state");
29
+ const requestUrl = getRequestURL(event);
30
+ const callbackUrl = `${requestUrl.origin}/api/auth/oauth/callback`;
31
+ const tokenUrl = new URL(privateConfig.tokenEndpoint, wordpressUrl);
32
+ const tokenResponse = await $fetch(tokenUrl.toString(), {
33
+ method: "POST",
34
+ headers: {
35
+ "Content-Type": "application/x-www-form-urlencoded"
36
+ },
37
+ body: new URLSearchParams({
38
+ grant_type: "authorization_code",
39
+ code,
40
+ redirect_uri: callbackUrl,
41
+ client_id: privateConfig.clientId,
42
+ client_secret: privateConfig.clientSecret
43
+ }).toString()
44
+ }).catch((error) => {
45
+ console.error("[WPNuxt Auth] Token exchange failed:", error);
46
+ throw createError({ statusCode: 500, message: "Failed to exchange authorization code" });
47
+ });
48
+ if (!tokenResponse.access_token) {
49
+ throw createError({ statusCode: 500, message: "No access token received" });
50
+ }
51
+ const userInfoUrl = new URL(privateConfig.userInfoEndpoint, wordpressUrl);
52
+ const userInfo = await $fetch(userInfoUrl.toString(), {
53
+ headers: {
54
+ Authorization: `Bearer ${tokenResponse.access_token}`
55
+ }
56
+ }).catch((error) => {
57
+ console.error("[WPNuxt Auth] User info fetch failed:", error);
58
+ return null;
59
+ });
60
+ const isProduction = process.env.NODE_ENV === "production";
61
+ setCookie(event, publicConfig.cookieName, tokenResponse.access_token, {
62
+ httpOnly: true,
63
+ secure: isProduction,
64
+ sameSite: "lax",
65
+ maxAge: tokenResponse.expires_in || publicConfig.tokenMaxAge,
66
+ path: "/"
67
+ });
68
+ if (tokenResponse.refresh_token) {
69
+ setCookie(event, publicConfig.refreshCookieName, tokenResponse.refresh_token, {
70
+ httpOnly: true,
71
+ secure: isProduction,
72
+ sameSite: "lax",
73
+ maxAge: publicConfig.refreshTokenMaxAge,
74
+ path: "/"
75
+ });
76
+ }
77
+ if (userInfo) {
78
+ const userId = userInfo.sub || userInfo.id || userInfo.ID;
79
+ const firstName = userInfo.given_name || userInfo.first_name || "";
80
+ const lastName = userInfo.family_name || userInfo.last_name || "";
81
+ const displayName = userInfo.name || userInfo.display_name || `${firstName} ${lastName}`.trim();
82
+ const username = userInfo.preferred_username || userInfo.username || userInfo.user_login || userInfo.nickname;
83
+ const avatarUrl = userInfo.picture || userInfo.avatar_url;
84
+ const userData = {
85
+ id: userId?.toString() || "",
86
+ databaseId: typeof userId === "number" ? userId : Number.parseInt(userId?.toString() || "0", 10),
87
+ email: userInfo.email,
88
+ name: displayName,
89
+ firstName,
90
+ lastName,
91
+ username,
92
+ nickname: userInfo.nickname,
93
+ avatar: avatarUrl ? { url: avatarUrl } : void 0
94
+ };
95
+ setCookie(event, "wpnuxt-oauth-user", JSON.stringify(userData), {
96
+ httpOnly: false,
97
+ // Client needs to read this
98
+ secure: isProduction,
99
+ sameSite: "lax",
100
+ maxAge: tokenResponse.expires_in || publicConfig.tokenMaxAge,
101
+ path: "/"
102
+ });
103
+ }
104
+ return sendRedirect(event, publicConfig.redirectOnLogin);
105
+ });
@@ -0,0 +1,81 @@
1
+ import { defineEventHandler, getRouterParam, setCookie, sendRedirect, createError, getRequestURL } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ export default defineEventHandler(async (event) => {
4
+ const provider = getRouterParam(event, "provider");
5
+ if (!provider) {
6
+ throw createError({ statusCode: 400, message: "Missing provider parameter" });
7
+ }
8
+ const config = useRuntimeConfig();
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
+ const graphqlUrl = `${wordpressUrl}${graphqlEndpoint}`;
16
+ const response = await $fetch(graphqlUrl, {
17
+ method: "POST",
18
+ headers: {
19
+ "Content-Type": "application/json"
20
+ },
21
+ body: {
22
+ query: `
23
+ query LoginClients {
24
+ loginClients {
25
+ name
26
+ provider
27
+ authorizationUrl
28
+ isEnabled
29
+ }
30
+ }
31
+ `
32
+ }
33
+ }).catch((error) => {
34
+ console.error("[WPNuxt Auth] Failed to fetch login clients:", error);
35
+ throw createError({ statusCode: 500, message: "Failed to fetch login providers from WordPress" });
36
+ });
37
+ if (response.errors?.length) {
38
+ console.error("[WPNuxt Auth] GraphQL errors:", response.errors);
39
+ throw createError({ statusCode: 500, message: response.errors[0]?.message || "GraphQL error" });
40
+ }
41
+ const loginClients = response.data?.loginClients || [];
42
+ const providerUpperCase = provider.toUpperCase();
43
+ const loginClient = loginClients.find(
44
+ (client) => client.provider === providerUpperCase && client.isEnabled
45
+ );
46
+ if (!loginClient) {
47
+ throw createError({
48
+ statusCode: 404,
49
+ message: `Provider '${provider}' not found or not enabled. Available providers: ${loginClients.filter((c) => c.isEnabled).map((c) => c.name).join(", ")}`
50
+ });
51
+ }
52
+ if (!loginClient.authorizationUrl) {
53
+ throw createError({
54
+ statusCode: 500,
55
+ message: `Provider '${provider}' does not have an authorization URL configured`
56
+ });
57
+ }
58
+ const state = crypto.randomUUID();
59
+ const isProduction = process.env.NODE_ENV === "production";
60
+ setCookie(event, "wpnuxt-headless-login-state", state, {
61
+ httpOnly: true,
62
+ secure: isProduction,
63
+ sameSite: "lax",
64
+ maxAge: 600,
65
+ // 10 minutes
66
+ path: "/"
67
+ });
68
+ setCookie(event, "wpnuxt-headless-login-provider", providerUpperCase, {
69
+ httpOnly: true,
70
+ secure: isProduction,
71
+ sameSite: "lax",
72
+ maxAge: 600,
73
+ path: "/"
74
+ });
75
+ const authUrl = new URL(loginClient.authorizationUrl);
76
+ authUrl.searchParams.set("state", state);
77
+ const requestUrl = getRequestURL(event);
78
+ const callbackUrl = `${requestUrl.origin}/api/auth/provider/${provider}/callback`;
79
+ authUrl.searchParams.set("redirect_uri", callbackUrl);
80
+ return sendRedirect(event, authUrl.toString());
81
+ });
@@ -0,0 +1,135 @@
1
+ import { defineEventHandler, getQuery, getRouterParam, getCookie, setCookie, deleteCookie, sendRedirect, createError } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ export default defineEventHandler(async (event) => {
4
+ const query = getQuery(event);
5
+ const routeProvider = getRouterParam(event, "provider");
6
+ const config = useRuntimeConfig();
7
+ const publicConfig = config.public.wpNuxtAuth;
8
+ const wordpressUrl = config.public.wordpressUrl;
9
+ const wpNuxtConfig = config.public.wpNuxt;
10
+ const graphqlEndpoint = wpNuxtConfig?.graphqlEndpoint || "/graphql";
11
+ if (!wordpressUrl) {
12
+ throw createError({ statusCode: 500, message: "WordPress URL not configured" });
13
+ }
14
+ if (query.error) {
15
+ const errorDesc = query.error_description || query.error;
16
+ throw createError({ statusCode: 400, message: `OAuth error: ${errorDesc}` });
17
+ }
18
+ const code = query.code;
19
+ if (!code) {
20
+ throw createError({ statusCode: 400, message: "Missing authorization code" });
21
+ }
22
+ const state = query.state;
23
+ const storedState = getCookie(event, "wpnuxt-headless-login-state");
24
+ if (!state || state !== storedState) {
25
+ throw createError({ statusCode: 400, message: "Invalid state parameter" });
26
+ }
27
+ const storedProvider = getCookie(event, "wpnuxt-headless-login-provider");
28
+ const provider = storedProvider || routeProvider?.toUpperCase();
29
+ if (!provider) {
30
+ throw createError({ statusCode: 400, message: "Missing provider information" });
31
+ }
32
+ deleteCookie(event, "wpnuxt-headless-login-state");
33
+ deleteCookie(event, "wpnuxt-headless-login-provider");
34
+ const graphqlUrl = `${wordpressUrl}${graphqlEndpoint}`;
35
+ const response = await $fetch(graphqlUrl, {
36
+ method: "POST",
37
+ headers: {
38
+ "Content-Type": "application/json"
39
+ },
40
+ body: {
41
+ query: `
42
+ mutation LoginWithProvider($provider: LoginProviderEnum!, $code: String!, $state: String) {
43
+ login(input: {
44
+ provider: $provider
45
+ oauthResponse: {
46
+ code: $code
47
+ state: $state
48
+ }
49
+ }) {
50
+ authToken
51
+ authTokenExpiration
52
+ refreshToken
53
+ refreshTokenExpiration
54
+ user {
55
+ id
56
+ databaseId
57
+ name
58
+ email
59
+ firstName
60
+ lastName
61
+ username
62
+ nickname
63
+ description
64
+ avatar {
65
+ url
66
+ }
67
+ roles {
68
+ nodes {
69
+ name
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ `,
76
+ variables: {
77
+ provider,
78
+ code,
79
+ state
80
+ }
81
+ }
82
+ }).catch((error) => {
83
+ console.error("[WPNuxt Auth] Login mutation failed:", error);
84
+ throw createError({ statusCode: 500, message: "Failed to authenticate with WordPress" });
85
+ });
86
+ if (response.errors?.length) {
87
+ console.error("[WPNuxt Auth] GraphQL errors:", response.errors);
88
+ throw createError({ statusCode: 400, message: response.errors[0]?.message || "Authentication failed" });
89
+ }
90
+ const loginData = response.data?.login;
91
+ if (!loginData?.authToken) {
92
+ throw createError({ statusCode: 500, message: "No auth token received from WordPress" });
93
+ }
94
+ const isProduction = process.env.NODE_ENV === "production";
95
+ setCookie(event, publicConfig.cookieName, loginData.authToken, {
96
+ httpOnly: true,
97
+ secure: isProduction,
98
+ sameSite: "lax",
99
+ maxAge: publicConfig.tokenMaxAge,
100
+ path: "/"
101
+ });
102
+ if (loginData.refreshToken) {
103
+ setCookie(event, publicConfig.refreshCookieName, loginData.refreshToken, {
104
+ httpOnly: true,
105
+ secure: isProduction,
106
+ sameSite: "lax",
107
+ maxAge: publicConfig.refreshTokenMaxAge,
108
+ path: "/"
109
+ });
110
+ }
111
+ if (loginData.user) {
112
+ const userData = {
113
+ id: loginData.user.id,
114
+ databaseId: loginData.user.databaseId,
115
+ email: loginData.user.email,
116
+ name: loginData.user.name,
117
+ firstName: loginData.user.firstName,
118
+ lastName: loginData.user.lastName,
119
+ username: loginData.user.username,
120
+ nickname: loginData.user.nickname,
121
+ description: loginData.user.description,
122
+ avatar: loginData.user.avatar,
123
+ roles: loginData.user.roles
124
+ };
125
+ setCookie(event, "wpnuxt-user", JSON.stringify(userData), {
126
+ httpOnly: false,
127
+ // Client needs to read this for hydration
128
+ secure: isProduction,
129
+ sameSite: "lax",
130
+ maxAge: publicConfig.tokenMaxAge,
131
+ path: "/"
132
+ });
133
+ }
134
+ return sendRedirect(event, publicConfig.redirectOnLogin);
135
+ });
@@ -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 }
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.2",
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.2"
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
  }