@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 +1 -1
- package/dist/module.mjs +193 -4
- package/dist/runtime/composables/useWPAuth.js +112 -12
- package/dist/runtime/composables/useWPUser.js +2 -1
- package/dist/runtime/plugins/auth.js +10 -0
- package/dist/runtime/queries/Auth.gql +111 -5
- package/dist/runtime/queries/Viewer.gql +26 -0
- package/dist/runtime/queries/fragments/GeneralSettings.fragment.gql +11 -0
- package/dist/runtime/server/api/auth/login.post.d.ts +0 -0
- package/dist/runtime/server/api/auth/login.post.js +51 -0
- package/dist/runtime/server/api/auth/logout.post.d.ts +0 -0
- package/dist/runtime/server/api/auth/logout.post.js +18 -0
- package/dist/runtime/server/api/auth/oauth/authorize.get.d.ts +0 -0
- package/dist/runtime/server/api/auth/oauth/authorize.get.js +36 -0
- package/dist/runtime/server/api/auth/oauth/callback.get.d.ts +0 -0
- package/dist/runtime/server/api/auth/oauth/callback.get.js +105 -0
- package/dist/runtime/server/api/auth/provider/[provider]/authorize.get.d.ts +0 -0
- package/dist/runtime/server/api/auth/provider/[provider]/authorize.get.js +81 -0
- package/dist/runtime/server/api/auth/provider/[provider]/callback.get.d.ts +0 -0
- package/dist/runtime/server/api/auth/provider/[provider]/callback.get.js +135 -0
- package/dist/runtime/types/nuxt-schema.d.ts +20 -0
- package/dist/runtime/types/stub.d.ts +17 -0
- package/package.json +3 -3
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,5 +1,99 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
83
|
+
if (!refreshTokenCookie.value) {
|
|
70
84
|
return false;
|
|
71
85
|
}
|
|
72
86
|
try {
|
|
73
|
-
const { data, errors } = await useGraphqlMutation("
|
|
74
|
-
refreshToken:
|
|
87
|
+
const { data, errors } = await useGraphqlMutation("RefreshToken", {
|
|
88
|
+
refreshToken: refreshTokenCookie.value
|
|
75
89
|
});
|
|
76
|
-
|
|
90
|
+
const refreshData = data;
|
|
91
|
+
if (errors?.length || !refreshData?.refreshToken?.success) {
|
|
77
92
|
await logout();
|
|
78
93
|
return false;
|
|
79
94
|
}
|
|
80
|
-
authToken.value =
|
|
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 =
|
|
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
|
|
1
|
+
# Login with username and password
|
|
2
|
+
# Requires: Headless Login for WPGraphQL plugin
|
|
2
3
|
mutation Login($username: String!, $password: String!) {
|
|
3
|
-
login(input: {
|
|
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
|
|
27
|
-
mutation
|
|
28
|
-
|
|
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
|
+
}
|
|
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
|
+
});
|
|
File without changes
|
|
@@ -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
|
+
});
|
|
File without changes
|
|
@@ -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
|
+
});
|
|
File without changes
|
|
@@ -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
|
+
});
|
|
File without changes
|
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
}
|