Refactor authentication flow to use 'signIn' instead of 'login' and enhance PAT handling with Logto integration
This commit is contained in:
parent
726a9adf1f
commit
ac3161c7b9
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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!
|
||||
}
|
||||
|
||||
@ -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" },
|
||||
|
||||
@ -40,6 +40,7 @@ export interface RegisterInput {
|
||||
export interface LoginInput {
|
||||
primaryEmail: string;
|
||||
password: string;
|
||||
createPat?: boolean;
|
||||
}
|
||||
|
||||
export interface LogtoCreateUserRequest {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user