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`
|
const LOGIN_MUTATION = gql`
|
||||||
mutation Login($input: LoginInput!) {
|
mutation Login($input: LoginInput!) {
|
||||||
login(input: $input) {
|
signIn(input: $input) {
|
||||||
success
|
success
|
||||||
message
|
message
|
||||||
user {
|
user {
|
||||||
@ -68,7 +68,7 @@ interface User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface LoginResponse {
|
interface LoginResponse {
|
||||||
login: {
|
signIn: {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
user?: User;
|
user?: User;
|
||||||
@ -117,7 +117,7 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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 [socialConnectors, setSocialConnectors] = useState<SocialConnector[]>([]);
|
||||||
const [connectorsLoading, setConnectorsLoading] = useState(false);
|
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
|
// Sauvegarder automatiquement le token d'accès dans localStorage
|
||||||
if (response.login.success && response.login.accessToken) {
|
if (response.signIn.success && response.signIn.accessToken) {
|
||||||
localStorage.setItem("accessToken", response.login.accessToken);
|
localStorage.setItem("accessToken", response.signIn.accessToken);
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";
|
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/auth";
|
||||||
import setAuth from "@/graphql/context/features/guard";
|
import initGuard from "@/graphql/context/features/guard";
|
||||||
import { patContextCache } from "@/graphql/context/features/patCache";
|
import { patContextCache } from "@/graphql/context/features/patCache";
|
||||||
import { GraphQLContext } from "@/graphql/context/types";
|
import { GraphQLContext } from "@/graphql/context/types";
|
||||||
import { YogaInitialContext } from "graphql-yoga";
|
import { YogaInitialContext } from "graphql-yoga";
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { AuthTypes } from "@/graphql/context";
|
|||||||
import { patContextCache } from "@/graphql/context/features/patCache";
|
import { patContextCache } from "@/graphql/context/features/patCache";
|
||||||
import { listResources, Resource } from "@/graphql/features/Resource/resourceService";
|
import { listResources, Resource } from "@/graphql/features/Resource/resourceService";
|
||||||
import { getUserRoles, Role } from "@/graphql/features/Role/roleService";
|
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)
|
// Fonction pour décoder un JWT (basique, sans vérification de signature)
|
||||||
function decodeJWT(token: string): { sub?: string; [key: string]: unknown } | null {
|
function decodeJWT(token: string): { sub?: string; [key: string]: unknown } | null {
|
||||||
@ -61,7 +62,6 @@ const setAuth = async (
|
|||||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||||
const token = authHeader.substring(7); // Retirer "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 userId: string | undefined = undefined;
|
||||||
let user: { id: string; name?: string; email?: string; lastName?: string; isActive?: boolean } | undefined =
|
let user: { id: string; name?: string; email?: string; lastName?: string; isActive?: boolean } | undefined =
|
||||||
undefined;
|
undefined;
|
||||||
@ -79,11 +79,27 @@ const setAuth = async (
|
|||||||
isActive: typeof decoded?.isActive === "boolean" ? decoded.isActive : undefined,
|
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
|
// PAT : tenter de récupérer l'userId via le cache
|
||||||
userId = patContextCache.getUserId(token) || undefined;
|
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) {
|
if (userId) {
|
||||||
user = { id: 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) : []
|
Array.isArray(r.permissions) ? r.permissions.map((p: { name: string }) => p.name) : []
|
||||||
);
|
);
|
||||||
resources = await listResources(minimalContext);
|
resources = await listResources(minimalContext);
|
||||||
} catch {
|
} catch (e) {
|
||||||
// ignore fetch errors, fallback to empty
|
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 {
|
return {
|
||||||
|
|||||||
@ -192,7 +192,14 @@ export const organizationResolvers = {
|
|||||||
context: GraphQLContext
|
context: GraphQLContext
|
||||||
): Promise<OrganizationsListResponse> {
|
): Promise<OrganizationsListResponse> {
|
||||||
try {
|
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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Authentification requise",
|
message: "Authentification requise",
|
||||||
@ -200,7 +207,7 @@ export const organizationResolvers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { page = 1, pageSize = 10 } = args;
|
const { page = 1, pageSize = 10 } = args;
|
||||||
const result = await getUserOrganizations(context.user.id, page, pageSize);
|
const result = await getUserOrganizations(userId, page, pageSize);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@ -21,6 +21,7 @@ input RegisterInput {
|
|||||||
input LoginInput {
|
input LoginInput {
|
||||||
primaryEmail: String!
|
primaryEmail: String!
|
||||||
password: String!
|
password: String!
|
||||||
|
createPat: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegisterResponse {
|
type RegisterResponse {
|
||||||
@ -58,6 +59,6 @@ type Query {
|
|||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
signUp(input: RegisterInput!): RegisterResponse!
|
signUp(input: RegisterInput!): RegisterResponse!
|
||||||
login(input: LoginInput!): LoginResponse!
|
signIn(input: LoginInput!): LoginResponse!
|
||||||
logout: LogoutResponse!
|
logout: LogoutResponse!
|
||||||
}
|
}
|
||||||
|
|||||||
@ -623,8 +623,6 @@ export const resolvers = {
|
|||||||
if (tokenData) {
|
if (tokenData) {
|
||||||
accessToken = tokenData.token;
|
accessToken = tokenData.token;
|
||||||
tokenExpiry = tokenData.expiresIn;
|
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);
|
patContextCache.register(accessToken, user.id, 24);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -638,49 +636,87 @@ export const resolvers = {
|
|||||||
accessToken,
|
accessToken,
|
||||||
tokenExpiry,
|
tokenExpiry,
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error) {
|
||||||
if (error instanceof GraphQLError) {
|
console.error("Erreur lors de l'inscription:", error);
|
||||||
throw 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);
|
// Erreur générique
|
||||||
throw new GraphQLError("Échec de l'inscription", {
|
throw new GraphQLError("Échec de l'inscription de l'utilisateur", {
|
||||||
extensions: { code: "REGISTRATION_FAILED" },
|
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 {
|
try {
|
||||||
if (!input.primaryEmail || !input.password) {
|
const { primaryEmail, password } = input;
|
||||||
throw new GraphQLError("L'email et le mot de passe sont requis", {
|
|
||||||
extensions: { code: "INVALID_INPUT" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentifier l'utilisateur
|
// Authentifier l'utilisateur via l'API Logto
|
||||||
const user = await authenticateUser(input.primaryEmail, input.password);
|
const user = await authenticateUser(primaryEmail, password);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
// Ajout d'un message d'erreur plus précis
|
||||||
success: false,
|
const userExists = await getUserByEmail(primaryEmail);
|
||||||
message: "Email ou mot de passe incorrect",
|
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 accessToken = undefined;
|
||||||
let tokenExpiry = undefined;
|
let tokenExpiry = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenData = await createPersonalAccessToken(user.id, context);
|
const tokenData = await createPersonalAccessToken(user.id, context);
|
||||||
if (tokenData) {
|
if (tokenData) {
|
||||||
accessToken = tokenData.token;
|
accessToken = tokenData.token;
|
||||||
tokenExpiry = tokenData.expiresIn;
|
tokenExpiry = tokenData.expiresIn;
|
||||||
|
context.patCache.register(accessToken, user.id, 24); // Enregistre dans le cache du contexte
|
||||||
patContextCache.register(accessToken, user.id, 24);
|
context.personalAccessToken = accessToken;
|
||||||
|
context.userId = user.id;
|
||||||
|
context.user = user;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
// On continue même si la création du token échoue
|
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 {
|
return {
|
||||||
@ -690,21 +726,19 @@ export const resolvers = {
|
|||||||
accessToken,
|
accessToken,
|
||||||
tokenExpiry,
|
tokenExpiry,
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error) {
|
||||||
if (error instanceof GraphQLError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("Erreur lors de la connexion:", error);
|
console.error("Erreur lors de la connexion:", error);
|
||||||
throw new GraphQLError("Échec de la connexion", {
|
return {
|
||||||
extensions: { code: "LOGIN_FAILED" },
|
success: false,
|
||||||
});
|
message: "Échec de la connexion",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async logout(_: unknown, __: unknown, context: GraphQLContext): Promise<LogoutResponse> {
|
async logout(_: unknown, __: unknown, context: GraphQLContext): Promise<LogoutResponse> {
|
||||||
try {
|
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) {
|
if (context.personalAccessToken) {
|
||||||
context.patCache.remove(context.personalAccessToken);
|
context.patCache.remove(context.personalAccessToken);
|
||||||
}
|
}
|
||||||
@ -713,7 +747,7 @@ export const resolvers = {
|
|||||||
success: true,
|
success: true,
|
||||||
message: "Déconnexion réussie",
|
message: "Déconnexion réussie",
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la déconnexion:", error);
|
console.error("Erreur lors de la déconnexion:", error);
|
||||||
throw new GraphQLError("Échec de la déconnexion", {
|
throw new GraphQLError("Échec de la déconnexion", {
|
||||||
extensions: { code: "LOGOUT_FAILED" },
|
extensions: { code: "LOGOUT_FAILED" },
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export interface RegisterInput {
|
|||||||
export interface LoginInput {
|
export interface LoginInput {
|
||||||
primaryEmail: string;
|
primaryEmail: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
createPat?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogtoCreateUserRequest {
|
export interface LogtoCreateUserRequest {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user