diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index fa47ae2..829eb5d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -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(null); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); const [socialConnectors, setSocialConnectors] = useState([]); 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"; diff --git a/src/graphql/context/context.ts b/src/graphql/context/context.ts index 84d6fbf..0be8ce9 100644 --- a/src/graphql/context/context.ts +++ b/src/graphql/context/context.ts @@ -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"; diff --git a/src/graphql/context/features/auth.ts b/src/graphql/context/features/auth.ts index 7893304..5cc369b 100644 --- a/src/graphql/context/features/auth.ts +++ b/src/graphql/context/features/auth.ts @@ -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 { diff --git a/src/graphql/features/Organization/resolver.ts b/src/graphql/features/Organization/resolver.ts index 7e8a50f..a763281 100644 --- a/src/graphql/features/Organization/resolver.ts +++ b/src/graphql/features/Organization/resolver.ts @@ -192,7 +192,14 @@ export const organizationResolvers = { context: GraphQLContext ): Promise { 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, diff --git a/src/graphql/features/User/User.graphql b/src/graphql/features/User/User.graphql index 3dd098e..4eb0907 100644 --- a/src/graphql/features/User/User.graphql +++ b/src/graphql/features/User/User.graphql @@ -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! } diff --git a/src/graphql/features/User/resolver.ts b/src/graphql/features/User/resolver.ts index 4672f21..ef38358 100644 --- a/src/graphql/features/User/resolver.ts +++ b/src/graphql/features/User/resolver.ts @@ -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 { + async signIn(_: unknown, { input }: { input: LoginInput }, context: GraphQLContext): Promise { 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 { 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" }, diff --git a/src/types/Logto.ts b/src/types/Logto.ts index 53bdc0c..6f5ade6 100644 --- a/src/types/Logto.ts +++ b/src/types/Logto.ts @@ -40,6 +40,7 @@ export interface RegisterInput { export interface LoginInput { primaryEmail: string; password: string; + createPat?: boolean; } export interface LogtoCreateUserRequest {