Refactor authentication flow to use 'signIn' instead of 'login' and enhance PAT handling with Logto integration

This commit is contained in:
Alexandre Bove 2025-07-29 10:03:36 +02:00
parent 726a9adf1f
commit ac3161c7b9
7 changed files with 111 additions and 51 deletions

View File

@ -8,7 +8,7 @@ const graphQLClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/
const LOGIN_MUTATION = gql`
mutation Login($input: LoginInput!) {
login(input: $input) {
signIn(input: $input) {
success
message
user {
@ -68,7 +68,7 @@ interface User {
}
interface LoginResponse {
login: {
signIn: {
success: boolean;
message?: string;
user?: User;
@ -117,7 +117,7 @@ export default function LoginPage() {
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<LoginResponse["login"] | null>(null);
const [result, setResult] = useState<LoginResponse["signIn"] | null>(null);
const [socialConnectors, setSocialConnectors] = useState<SocialConnector[]>([]);
const [connectorsLoading, setConnectorsLoading] = useState(false);
@ -195,11 +195,11 @@ export default function LoginPage() {
},
});
setResult(response.login);
setResult(response.signIn);
// Sauvegarder automatiquement le token d'accès dans localStorage
if (response.login.success && response.login.accessToken) {
localStorage.setItem("accessToken", response.login.accessToken);
if (response.signIn.success && response.signIn.accessToken) {
localStorage.setItem("accessToken", response.signIn.accessToken);
}
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";

View File

@ -1,5 +1,5 @@
import initGuard from "@/graphql/context/features/auth";
import setAuth from "@/graphql/context/features/guard";
import setAuth from "@/graphql/context/features/auth";
import initGuard from "@/graphql/context/features/guard";
import { patContextCache } from "@/graphql/context/features/patCache";
import { GraphQLContext } from "@/graphql/context/types";
import { YogaInitialContext } from "graphql-yoga";

View File

@ -2,6 +2,7 @@ import { AuthTypes } from "@/graphql/context";
import { patContextCache } from "@/graphql/context/features/patCache";
import { listResources, Resource } from "@/graphql/features/Resource/resourceService";
import { getUserRoles, Role } from "@/graphql/features/Role/roleService";
import { validatePATWithLogtoAPI } from "@/graphql/features/User/resolver";
// Fonction pour décoder un JWT (basique, sans vérification de signature)
function decodeJWT(token: string): { sub?: string; [key: string]: unknown } | null {
@ -61,7 +62,6 @@ const setAuth = async (
if (authHeader && authHeader.startsWith("Bearer ")) {
const token = authHeader.substring(7); // Retirer "Bearer "
// Décoder le JWT pour récupérer l'userId (seulement si c'est un JWT)
let userId: string | undefined = undefined;
let user: { id: string; name?: string; email?: string; lastName?: string; isActive?: boolean } | undefined =
undefined;
@ -79,11 +79,27 @@ const setAuth = async (
isActive: typeof decoded?.isActive === "boolean" ? decoded.isActive : undefined,
};
}
} else {
// Log JWT
console.error("[setAuth] JWT decoded:", decoded);
} else if (token.startsWith("pat_")) {
// PAT : tenter de récupérer l'userId via le cache
userId = patContextCache.getUserId(token) || undefined;
if (!userId) {
// Si non trouvé dans le cache, tente une validation via Logto
try {
userId = await validatePATWithLogtoAPI(token, { patCache: patContextCache, db: {}, request: req });
if (userId) {
patContextCache.register(token, userId, 24);
}
} catch (e) {
console.error("[setAuth] PAT non reconnu via Logto:", token, e);
}
}
console.error("[setAuth] PAT lookup:", { token, userId });
if (userId) {
user = { id: userId };
} else {
console.error("[setAuth] PAT non reconnu dans le cache ni via Logto, token:", token);
}
}
@ -103,8 +119,9 @@ const setAuth = async (
Array.isArray(r.permissions) ? r.permissions.map((p: { name: string }) => p.name) : []
);
resources = await listResources(minimalContext);
} catch {
// ignore fetch errors, fallback to empty
} catch (e) {
console.error("[setAuth] Erreur lors de la récupération des rôles/ressources:", e);
// On continue avec userId et user, mais rôles/permissions/ressources vides
}
}
return {

View File

@ -192,7 +192,14 @@ export const organizationResolvers = {
context: GraphQLContext
): Promise<OrganizationsListResponse> {
try {
if (!context.user) {
// DEBUG: log du contexte d'authentification
console.log("[myOrganizations] context.user:", context.user);
console.log("[myOrganizations] context.userId:", context.userId);
if (context.request && typeof context.request.headers?.get === "function") {
console.log("[myOrganizations] Authorization header:", context.request.headers.get("Authorization"));
}
const userId = context.user?.id || context.userId;
if (!userId) {
return {
success: false,
message: "Authentification requise",
@ -200,7 +207,7 @@ export const organizationResolvers = {
}
const { page = 1, pageSize = 10 } = args;
const result = await getUserOrganizations(context.user.id, page, pageSize);
const result = await getUserOrganizations(userId, page, pageSize);
return {
success: true,

View File

@ -21,6 +21,7 @@ input RegisterInput {
input LoginInput {
primaryEmail: String!
password: String!
createPat: Boolean
}
type RegisterResponse {
@ -58,6 +59,6 @@ type Query {
type Mutation {
signUp(input: RegisterInput!): RegisterResponse!
login(input: LoginInput!): LoginResponse!
signIn(input: LoginInput!): LoginResponse!
logout: LogoutResponse!
}

View File

@ -623,8 +623,6 @@ export const resolvers = {
if (tokenData) {
accessToken = tokenData.token;
tokenExpiry = tokenData.expiresIn;
// Enregistrement global du PAT pour tous les contextes
const { patContextCache } = await import("@/graphql/context/features/patCache");
patContextCache.register(accessToken, user.id, 24);
}
} catch {
@ -638,49 +636,87 @@ export const resolvers = {
accessToken,
tokenExpiry,
};
} catch (error: unknown) {
if (error instanceof GraphQLError) {
throw error;
} catch (error) {
console.error("Erreur lors de l'inscription:", error);
// Gérer les erreurs spécifiques d'Axios
if (axios.isAxiosError(error) && error.response) {
const { status, data } = error.response;
// Erreur 422 : Données invalides
if (status === 422) {
const errorCode = data?.code;
if (errorCode === "user.username_already_in_use") {
throw new GraphQLError("Ce nom d'utilisateur est déjà utilisé", {
extensions: { code: "USERNAME_ALREADY_EXISTS" },
});
} else if (errorCode === "user.email_already_in_use") {
throw new GraphQLError("Un utilisateur avec cet email existe déjà", {
extensions: { code: "USER_ALREADY_EXISTS" },
});
} else {
throw new GraphQLError("Les données fournies ne sont pas valides", {
extensions: { code: "INVALID_INPUT" },
});
}
}
// Erreur 400 : Mauvaises requêtes
if (status === 400) {
const errorMessage = data?.message || "Données invalides";
throw new GraphQLError(`Erreur de validation: ${errorMessage}`, {
extensions: { code: "VALIDATION_ERROR" },
});
}
}
console.error("Erreur lors de l'inscription:", error);
throw new GraphQLError("Échec de l'inscription", {
extensions: { code: "REGISTRATION_FAILED" },
// Erreur générique
throw new GraphQLError("Échec de l'inscription de l'utilisateur", {
extensions: { code: "USER_CREATION_FAILED" },
});
}
},
async login(_: unknown, { input }: { input: LoginInput }, context: GraphQLContext): Promise<LoginResponse> {
async signIn(_: unknown, { input }: { input: LoginInput }, context: GraphQLContext): Promise<LoginResponse> {
try {
if (!input.primaryEmail || !input.password) {
throw new GraphQLError("L'email et le mot de passe sont requis", {
extensions: { code: "INVALID_INPUT" },
});
}
const { primaryEmail, password } = input;
// Authentifier l'utilisateur
const user = await authenticateUser(input.primaryEmail, input.password);
// Authentifier l'utilisateur via l'API Logto
const user = await authenticateUser(primaryEmail, password);
if (!user) {
return {
success: false,
message: "Email ou mot de passe incorrect",
};
// Ajout d'un message d'erreur plus précis
const userExists = await getUserByEmail(primaryEmail);
if (!userExists) {
return {
success: false,
message: "Utilisateur inexistant ou email incorrect",
};
} else {
return {
success: false,
message: "Mot de passe incorrect ou utilisateur créé via social login (pas de mot de passe)",
};
}
}
// Créer un nouveau token d'accès personnel pour cette session
// Création automatique du PAT à chaque connexion
let accessToken = undefined;
let tokenExpiry = undefined;
try {
const tokenData = await createPersonalAccessToken(user.id, context);
if (tokenData) {
accessToken = tokenData.token;
tokenExpiry = tokenData.expiresIn;
patContextCache.register(accessToken, user.id, 24);
context.patCache.register(accessToken, user.id, 24); // Enregistre dans le cache du contexte
context.personalAccessToken = accessToken;
context.userId = user.id;
context.user = user;
}
} catch {
// On continue même si la création du token échoue
} catch (error) {
console.error("Erreur lors de la création du PAT à la connexion:", error);
// Même si le PAT échoue, on renseigne le contexte utilisateur
context.userId = user.id;
context.user = user;
}
return {
@ -690,21 +726,19 @@ export const resolvers = {
accessToken,
tokenExpiry,
};
} catch (error: unknown) {
if (error instanceof GraphQLError) {
throw error;
}
} catch (error) {
console.error("Erreur lors de la connexion:", error);
throw new GraphQLError("Échec de la connexion", {
extensions: { code: "LOGIN_FAILED" },
});
return {
success: false,
message: "Échec de la connexion",
};
}
},
async logout(_: unknown, __: unknown, context: GraphQLContext): Promise<LogoutResponse> {
try {
// Si un token est présent dans le contexte, le supprimer du cache
// Ici, vous pouvez ajouter des logiques de nettoyage si nécessaire
// Par exemple, supprimer le token d'accès personnel du cache
if (context.personalAccessToken) {
context.patCache.remove(context.personalAccessToken);
}
@ -713,7 +747,7 @@ export const resolvers = {
success: true,
message: "Déconnexion réussie",
};
} catch (error: unknown) {
} catch (error) {
console.error("Erreur lors de la déconnexion:", error);
throw new GraphQLError("Échec de la déconnexion", {
extensions: { code: "LOGOUT_FAILED" },

View File

@ -40,6 +40,7 @@ export interface RegisterInput {
export interface LoginInput {
primaryEmail: string;
password: string;
createPat?: boolean;
}
export interface LogtoCreateUserRequest {