feat: Initial authentication logic and UX

This commit is contained in:
2026-01-13 21:07:11 -05:00
parent f9958701e6
commit c1094239a3
26 changed files with 715 additions and 9 deletions

View 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 };
}
});

View 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),
};
});

View File

@@ -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 darticle par défaut."""

View 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);
}
}
},
});

View 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;
}