feat: Initial authentication logic and UX
This commit is contained in:
12
wp-content/themes/moonshine/server/api/logout.post.ts
Normal file
12
wp-content/themes/moonshine/server/api/logout.post.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineEventHandler } from "h3";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await handleLogout(event);
|
||||
return { success: true, message: "Déconnexion réussie" };
|
||||
}
|
||||
catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Une erreur est survenue lors de la déconnexion.";
|
||||
return { success: false, message };
|
||||
}
|
||||
});
|
||||
11
wp-content/themes/moonshine/server/graphql/context.ts
Normal file
11
wp-content/themes/moonshine/server/graphql/context.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineGraphQLContext } from "@lewebsimple/nuxt-graphql/helpers";
|
||||
import type { AuthLoginMutation } from "#graphql/typed-documents";
|
||||
|
||||
export default defineGraphQLContext(async (event) => {
|
||||
const authToken = await getAuthToken(event);
|
||||
|
||||
return {
|
||||
authToken,
|
||||
handleLogin: async (loginData: AuthLoginMutation) => handleLogin(event, loginData),
|
||||
};
|
||||
});
|
||||
@@ -39,6 +39,18 @@ type ACFE_AdvancedLink_Url implements ACFE_AdvancedLink {
|
||||
url: String
|
||||
}
|
||||
|
||||
"""A Field Group managed by ACF"""
|
||||
interface AcfFieldGroup {
|
||||
"""The name of the field group"""
|
||||
fieldGroupName: String @deprecated(reason: "Use __typename instead")
|
||||
}
|
||||
|
||||
"""Fields associated with an ACF Field Group"""
|
||||
interface AcfFieldGroupFields {
|
||||
"""The name of the field group"""
|
||||
fieldGroupName: String @deprecated(reason: "Use __typename instead")
|
||||
}
|
||||
|
||||
"""The Headless Login authentication data."""
|
||||
type AuthenticationData {
|
||||
"""A new authentication token to use in future requests."""
|
||||
@@ -3295,6 +3307,92 @@ enum GoogleProviderPromptTypeEnum {
|
||||
SELECT_ACCOUNT
|
||||
}
|
||||
|
||||
"""
|
||||
The "GroupAbstractBuilder" Field Group. Added to the Schema by "WPGraphQL for ACF".
|
||||
"""
|
||||
type GroupAbstractBuilder implements AcfFieldGroup & AcfFieldGroupFields & GroupAbstractBuilder_Fields {
|
||||
"""The name of the field group"""
|
||||
fieldGroupName: String @deprecated(reason: "Use __typename instead")
|
||||
|
||||
"""
|
||||
Field of the "flexible_content" Field Type added to the schema as part of the "GroupAbstractBuilder" Field Group
|
||||
"""
|
||||
sections: [GroupAbstractBuilderSections_Layout]
|
||||
}
|
||||
|
||||
"""
|
||||
The "GroupAbstractBuilderSectionsTextBlockLayout" Field Group. Added to the Schema by "WPGraphQL for ACF".
|
||||
"""
|
||||
type GroupAbstractBuilderSectionsTextBlockLayout implements AcfFieldGroup & AcfFieldGroupFields & GroupAbstractBuilderSectionsTextBlockLayout_Fields & GroupAbstractBuilderSections_Layout {
|
||||
"""
|
||||
Field of the "wysiwyg" Field Type added to the schema as part of the "GroupAbstractBuilderSectionsTextBlockLayout" Field Group
|
||||
"""
|
||||
content: String!
|
||||
|
||||
"""The name of the field group"""
|
||||
fieldGroupName: String @deprecated(reason: "Use __typename instead")
|
||||
}
|
||||
|
||||
"""
|
||||
Interface representing fields of the ACF "GroupAbstractBuilderSectionsTextBlockLayout" Field Group
|
||||
"""
|
||||
interface GroupAbstractBuilderSectionsTextBlockLayout_Fields implements AcfFieldGroup & AcfFieldGroupFields & GroupAbstractBuilderSections_Layout {
|
||||
"""
|
||||
Field of the "wysiwyg" Field Type added to the schema as part of the "GroupAbstractBuilderSectionsTextBlockLayout" Field Group
|
||||
"""
|
||||
content: String!
|
||||
|
||||
"""The name of the field group"""
|
||||
fieldGroupName: String @deprecated(reason: "Use __typename instead")
|
||||
}
|
||||
|
||||
"""
|
||||
Layout of the "sections" Field of the "GroupAbstractBuilder" Field Group Field
|
||||
"""
|
||||
interface GroupAbstractBuilderSections_Layout {
|
||||
"""The name of the ACF Flex Field Layout"""
|
||||
fieldGroupName: String
|
||||
}
|
||||
|
||||
"""
|
||||
Interface representing fields of the ACF "GroupAbstractBuilder" Field Group
|
||||
"""
|
||||
interface GroupAbstractBuilder_Fields implements AcfFieldGroup & AcfFieldGroupFields {
|
||||
"""The name of the field group"""
|
||||
fieldGroupName: String @deprecated(reason: "Use __typename instead")
|
||||
|
||||
"""
|
||||
Field of the "flexible_content" Field Type added to the schema as part of the "GroupAbstractBuilder" Field Group
|
||||
"""
|
||||
sections: [GroupAbstractBuilderSections_Layout]
|
||||
}
|
||||
|
||||
"""
|
||||
The "GroupPostPage" Field Group. Added to the Schema by "WPGraphQL for ACF".
|
||||
"""
|
||||
type GroupPostPage implements AcfFieldGroup & AcfFieldGroupFields & GroupAbstractBuilder_Fields & GroupPostPage_Fields {
|
||||
"""The name of the field group"""
|
||||
fieldGroupName: String @deprecated(reason: "Use __typename instead")
|
||||
|
||||
"""
|
||||
Field of the "flexible_content" Field Type added to the schema as part of the "GroupAbstractBuilder" Field Group
|
||||
"""
|
||||
sections: [GroupAbstractBuilderSections_Layout]
|
||||
}
|
||||
|
||||
"""
|
||||
Interface representing fields of the ACF "GroupPostPage" Field Group
|
||||
"""
|
||||
interface GroupPostPage_Fields implements AcfFieldGroup & AcfFieldGroupFields & GroupAbstractBuilder_Fields {
|
||||
"""The name of the field group"""
|
||||
fieldGroupName: String @deprecated(reason: "Use __typename instead")
|
||||
|
||||
"""
|
||||
Field of the "flexible_content" Field Type added to the schema as part of the "GroupAbstractBuilder" Field Group
|
||||
"""
|
||||
sections: [GroupAbstractBuilderSections_Layout]
|
||||
}
|
||||
|
||||
"""
|
||||
Content that can be organized in a parent-child structure. Provides fields for navigating up and down the hierarchy and maintaining structured relationships.
|
||||
"""
|
||||
@@ -5759,7 +5857,7 @@ enum OrderEnum {
|
||||
"""
|
||||
A standalone content entry generally used for static, non-chronological content such as "About Us" or "Contact" pages.
|
||||
"""
|
||||
type Page implements ContentNode & DatabaseIdentifier & HierarchicalContentNode & HierarchicalNode & MenuItemLinkable & Node & NodeWithAuthor & NodeWithContentEditor & NodeWithFeaturedImage & NodeWithPageAttributes & NodeWithRevisions & NodeWithTemplate & NodeWithTitle & Previewable & UniformResourceIdentifiable {
|
||||
type Page implements ContentNode & DatabaseIdentifier & HierarchicalContentNode & HierarchicalNode & MenuItemLinkable & Node & NodeWithAuthor & NodeWithContentEditor & NodeWithFeaturedImage & NodeWithPageAttributes & NodeWithRevisions & NodeWithTemplate & NodeWithTitle & Previewable & UniformResourceIdentifiable & WithAcfGroupPostPage {
|
||||
"""
|
||||
Returns ancestors of the node. Default ordered as lowest (closest to the child) to highest (closest to the root).
|
||||
"""
|
||||
@@ -5902,6 +6000,9 @@ type Page implements ContentNode & DatabaseIdentifier & HierarchicalContentNode
|
||||
"""Globally unique ID of the featured image assigned to the node"""
|
||||
featuredImageId: ID
|
||||
|
||||
"""Fields of the GroupPostPage ACF Field Group"""
|
||||
groupPostPage: GroupPostPage
|
||||
|
||||
"""
|
||||
The global unique identifier for this post. This currently matches the value stored in WP_Post->guid and the guid column in the "post_objects" database table.
|
||||
"""
|
||||
@@ -13884,6 +13985,14 @@ interface WPPageInfo implements PageInfo {
|
||||
startCursor: String
|
||||
}
|
||||
|
||||
"""
|
||||
Provides access to fields of the "GroupPostPage" ACF Field Group via the "groupPostPage" field
|
||||
"""
|
||||
interface WithAcfGroupPostPage {
|
||||
"""Fields of the GroupPostPage ACF Field Group"""
|
||||
groupPostPage: GroupPostPage
|
||||
}
|
||||
|
||||
"""The writing setting type"""
|
||||
type WritingSettings {
|
||||
"""Catégorie d’article par défaut."""
|
||||
|
||||
20
wp-content/themes/moonshine/server/graphql/wp-middleware.ts
Normal file
20
wp-content/themes/moonshine/server/graphql/wp-middleware.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { AuthLoginMutation } from "#build/graphql/typed-documents";
|
||||
import { defineRemoteExecMiddleware } from "@lewebsimple/nuxt-graphql/helpers";
|
||||
|
||||
export default defineRemoteExecMiddleware({
|
||||
onRequest({ context, fetchOptions }) {
|
||||
// Attach auth token from context to request headers
|
||||
if (context.authToken) {
|
||||
fetchOptions.headers.set("Authorization", `Bearer ${context.authToken}`);
|
||||
}
|
||||
},
|
||||
async onResponse({ operationName, response, context }) {
|
||||
// Save auth token in user session
|
||||
if (operationName === "AuthLogin") {
|
||||
const { data } = await response.json() as { data?: AuthLoginMutation };
|
||||
if (data) {
|
||||
await context.handleLogin(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
75
wp-content/themes/moonshine/server/utils/auth.ts
Normal file
75
wp-content/themes/moonshine/server/utils/auth.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { H3Event } from "h3";
|
||||
import { GraphQLClient } from "graphql-request";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import type { User } from "#auth-utils";
|
||||
import { type AuthUserFragment, type AuthLoginMutation, AuthRefreshTokenDocument } from "#graphql/typed-documents";
|
||||
|
||||
// Handle login result and store user session
|
||||
export async function handleLogin(event: H3Event, loginData: AuthLoginMutation) {
|
||||
if (!loginData?.login) {
|
||||
return;
|
||||
}
|
||||
const { user, authToken, refreshToken } = loginData.login;
|
||||
if (!user || !authToken || !refreshToken) {
|
||||
return;
|
||||
}
|
||||
await setUserSession(event, {
|
||||
user: getAuthUser(user),
|
||||
secure: {
|
||||
authToken,
|
||||
refreshToken,
|
||||
},
|
||||
loggedInAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Handle user logout by clearing session
|
||||
export async function handleLogout(event: H3Event) {
|
||||
await clearUserSession(event);
|
||||
}
|
||||
|
||||
// Convert AuthUserFragment to nuxt-auth-utils User
|
||||
function getAuthUser(user: AuthUserFragment): User {
|
||||
return {
|
||||
id: Number(user.id),
|
||||
email: user.email!,
|
||||
roles: extractNodes(user.roles).map(({ name }) => name!) || [],
|
||||
};
|
||||
}
|
||||
|
||||
// Refresh auth token by calling remote GraphQL endpoint directly
|
||||
export async function refreshAuthToken(refreshToken: string): Promise<string | undefined> {
|
||||
const client = new GraphQLClient(`${process.env.NUXT_WP_URL || "https://wp-headless.ledevsimple.ca"}/graphql`);
|
||||
const data = await client.request(AuthRefreshTokenDocument, { refreshToken });
|
||||
return data.refreshToken?.authToken || undefined;
|
||||
}
|
||||
|
||||
// Get auth token from user session (refresh if needed)
|
||||
export async function getAuthToken(event: H3Event): Promise<string | undefined> {
|
||||
// Retrieve user session, return if none
|
||||
const session = await getUserSession(event);
|
||||
if (!session.secure) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract tokens and check expiration
|
||||
const { authToken, refreshToken } = session.secure;
|
||||
const decoded = jwtDecode<{ exp: number }>(authToken);
|
||||
const isExpired = decoded.exp * 1000 < Date.now();
|
||||
if (isExpired) {
|
||||
try {
|
||||
const newAuthToken = await refreshAuthToken(refreshToken);
|
||||
if (!newAuthToken) {
|
||||
throw new Error("Impossible de rafraîchir le jeton d'authentification.");
|
||||
}
|
||||
session.secure.authToken = newAuthToken;
|
||||
await setUserSession(event, session);
|
||||
}
|
||||
catch {
|
||||
await clearUserSession(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return session.secure.authToken;
|
||||
}
|
||||
Reference in New Issue
Block a user