Enhance authentication and authorization features with Logto integration
- Enrich context with user roles, permissions, and resources from Logto. - Update setAuth function to handle JWT decoding and user data retrieval. - Refactor initGuard to utilize new auth structure. - Modify resource and role services to support new data types and error handling.
This commit is contained in:
parent
b59fa5e267
commit
726a9adf1f
@ -1,9 +1,130 @@
|
||||
import { GuardFunction } from "@/graphql/context/types";
|
||||
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";
|
||||
|
||||
const initGuard: GuardFunction = auth => {
|
||||
// Fonction pour décoder un JWT (basique, sans vérification de signature)
|
||||
function decodeJWT(token: string): { sub?: string; [key: string]: unknown } | null {
|
||||
try {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const payload = parts[1];
|
||||
const decoded = Buffer.from(payload, "base64url").toString("utf8");
|
||||
return JSON.parse(decoded) as { sub?: string; [key: string]: unknown };
|
||||
} catch (error) {
|
||||
console.warn("Impossible de décoder le JWT:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const setAuth = async (
|
||||
req:
|
||||
| Request
|
||||
| {
|
||||
headers?: Headers | Record<string, string>;
|
||||
get?: (header: string) => string | undefined;
|
||||
header?: (header: string) => string | undefined;
|
||||
}
|
||||
): Promise<AuthTypes & { roles?: Role[]; permissions?: string[]; resources?: Resource[] }> => {
|
||||
// Extraire le token d'autorisation des headers
|
||||
let authHeader: string | null = null;
|
||||
|
||||
// GraphQL Yoga utilise la structure Request Web API
|
||||
if (req.headers) {
|
||||
// Si c'est un objet Headers (Web API)
|
||||
if (typeof req.headers.get === "function") {
|
||||
authHeader = (req.headers as Headers).get("Authorization");
|
||||
}
|
||||
// Si c'est un objet record (GraphQL Yoga parfois)
|
||||
else if (typeof req.headers === "object") {
|
||||
authHeader =
|
||||
(req.headers as Record<string, string>)["authorization"] ||
|
||||
(req.headers as Record<string, string>)["Authorization"];
|
||||
}
|
||||
} else {
|
||||
// Essayer d'accéder aux headers via une autre méthode
|
||||
try {
|
||||
// Tenter d'utiliser req comme un objet Request ou custom
|
||||
if (typeof (req as { get?: (header: string) => string | undefined }).get === "function") {
|
||||
const val = (req as { get: (header: string) => string | undefined }).get("Authorization");
|
||||
authHeader = val ?? null;
|
||||
} else if (typeof (req as { header?: (header: string) => string | undefined }).header === "function") {
|
||||
const val = (req as { header: (header: string) => string | undefined }).header("Authorization");
|
||||
authHeader = val ?? null;
|
||||
}
|
||||
} catch {
|
||||
// Ignorer les erreurs d'accès aux headers
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (token.includes(".") && token.split(".").length === 3) {
|
||||
// C'est un JWT
|
||||
const decoded = decodeJWT(token);
|
||||
userId = decoded?.sub as string | undefined;
|
||||
if (userId) {
|
||||
user = {
|
||||
id: userId,
|
||||
name: typeof decoded?.name === "string" ? decoded.name : undefined,
|
||||
email: typeof decoded?.email === "string" ? decoded.email : undefined,
|
||||
lastName: typeof decoded?.family_name === "string" ? decoded.family_name : undefined,
|
||||
isActive: typeof decoded?.isActive === "boolean" ? decoded.isActive : undefined,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// PAT : tenter de récupérer l'userId via le cache
|
||||
userId = patContextCache.getUserId(token) || undefined;
|
||||
if (userId) {
|
||||
user = { id: userId };
|
||||
}
|
||||
}
|
||||
|
||||
let roles: Role[] = [];
|
||||
let permissions: string[] = [];
|
||||
let resources: Resource[] = [];
|
||||
if (userId) {
|
||||
try {
|
||||
const minimalContext = {
|
||||
accessToken: token,
|
||||
request: req as Request,
|
||||
db: {},
|
||||
patCache: patContextCache,
|
||||
};
|
||||
roles = await getUserRoles(userId, minimalContext);
|
||||
permissions = roles.flatMap(r =>
|
||||
Array.isArray(r.permissions) ? r.permissions.map((p: { name: string }) => p.name) : []
|
||||
);
|
||||
resources = await listResources(minimalContext);
|
||||
} catch {
|
||||
// ignore fetch errors, fallback to empty
|
||||
}
|
||||
}
|
||||
return {
|
||||
accessToken: token,
|
||||
personalAccessToken: token,
|
||||
userId: userId,
|
||||
user: user,
|
||||
roles,
|
||||
permissions,
|
||||
resources,
|
||||
};
|
||||
}
|
||||
|
||||
// Pas d'authentification
|
||||
return {
|
||||
isAuthenticated: !!auth?.accessToken,
|
||||
accessToken: undefined,
|
||||
personalAccessToken: undefined,
|
||||
userId: undefined,
|
||||
user: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export default initGuard;
|
||||
export default setAuth;
|
||||
|
||||
@ -1,105 +1,18 @@
|
||||
import { AuthTypes } from "@/graphql/context";
|
||||
import { patContextCache } from "@/graphql/context/features/patCache";
|
||||
import { GuardFunction } from "@/graphql/context/types";
|
||||
|
||||
// Fonction pour décoder un JWT (basique, sans vérification de signature)
|
||||
function decodeJWT(token: string): { sub?: string; [key: string]: unknown } | null {
|
||||
try {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const payload = parts[1];
|
||||
const decoded = Buffer.from(payload, "base64url").toString("utf8");
|
||||
return JSON.parse(decoded) as { sub?: string; [key: string]: unknown };
|
||||
} catch (error) {
|
||||
console.warn("Impossible de décoder le JWT:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const setAuth = async (
|
||||
req:
|
||||
| Request
|
||||
| {
|
||||
headers?: Headers | Record<string, string>;
|
||||
get?: (header: string) => string | undefined;
|
||||
header?: (header: string) => string | undefined;
|
||||
}
|
||||
): Promise<AuthTypes> => {
|
||||
// Extraire le token d'autorisation des headers
|
||||
let authHeader: string | null = null;
|
||||
|
||||
// GraphQL Yoga utilise la structure Request Web API
|
||||
if (req.headers) {
|
||||
// Si c'est un objet Headers (Web API)
|
||||
if (typeof req.headers.get === "function") {
|
||||
authHeader = (req.headers as Headers).get("Authorization");
|
||||
}
|
||||
// Si c'est un objet record (GraphQL Yoga parfois)
|
||||
else if (typeof req.headers === "object") {
|
||||
authHeader =
|
||||
(req.headers as Record<string, string>)["authorization"] ||
|
||||
(req.headers as Record<string, string>)["Authorization"];
|
||||
}
|
||||
} else {
|
||||
// Essayer d'accéder aux headers via une autre méthode
|
||||
try {
|
||||
// Tenter d'utiliser req comme un objet Request ou custom
|
||||
if (typeof (req as { get?: (header: string) => string | undefined }).get === "function") {
|
||||
const val = (req as { get: (header: string) => string | undefined }).get("Authorization");
|
||||
authHeader = val ?? null;
|
||||
} else if (typeof (req as { header?: (header: string) => string | undefined }).header === "function") {
|
||||
const val = (req as { header: (header: string) => string | undefined }).header("Authorization");
|
||||
authHeader = val ?? null;
|
||||
}
|
||||
} catch {
|
||||
// Ignorer les erreurs d'accès aux headers
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (token.includes(".") && token.split(".").length === 3) {
|
||||
// C'est un JWT
|
||||
const decoded = decodeJWT(token);
|
||||
userId = decoded?.sub as string | undefined;
|
||||
if (userId) {
|
||||
user = {
|
||||
id: userId,
|
||||
name: typeof decoded?.name === "string" ? decoded.name : undefined,
|
||||
email: typeof decoded?.email === "string" ? decoded.email : undefined,
|
||||
lastName: typeof decoded?.family_name === "string" ? decoded.family_name : undefined,
|
||||
isActive: typeof decoded?.isActive === "boolean" ? decoded.isActive : undefined,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// PAT : tenter de récupérer l'userId via le cache
|
||||
userId = patContextCache.getUserId(token) || undefined;
|
||||
if (userId) {
|
||||
user = { id: userId };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: token,
|
||||
personalAccessToken: token,
|
||||
userId: userId,
|
||||
user: user, // <-- Correction ici
|
||||
};
|
||||
}
|
||||
|
||||
// Pas d'authentification
|
||||
const initGuard: GuardFunction = auth => {
|
||||
return {
|
||||
accessToken: undefined,
|
||||
personalAccessToken: undefined,
|
||||
userId: undefined,
|
||||
user: undefined,
|
||||
isAuthenticated: !!auth?.accessToken,
|
||||
hasRole: (roleName: string) => {
|
||||
return Array.isArray(auth?.roles) && auth.roles.some(r => r.name === roleName);
|
||||
},
|
||||
hasPermission: (permissionName: string) => {
|
||||
return Array.isArray(auth?.permissions) && auth.permissions.includes(permissionName);
|
||||
},
|
||||
hasResource: (resourceName: string) => {
|
||||
return Array.isArray(auth?.resources) && auth.resources.some(r => r.name === resourceName);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default setAuth;
|
||||
export default initGuard;
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import type { Resource } from "@/graphql/features/Resource/resourceService";
|
||||
import type { Role } from "@/graphql/features/Role/roleService";
|
||||
|
||||
export interface DB {
|
||||
// Define your database types here
|
||||
}
|
||||
@ -19,6 +22,9 @@ export interface AuthTypes {
|
||||
accessToken?: string;
|
||||
personalAccessToken?: string; // Token d'accès personnel Logto
|
||||
userId?: string; // ID utilisateur pour les requêtes authentifiées
|
||||
roles?: Role[];
|
||||
permissions?: string[];
|
||||
resources?: Resource[];
|
||||
}
|
||||
|
||||
export interface PATCache {
|
||||
|
||||
@ -42,10 +42,10 @@ export const resourceResolvers = {
|
||||
},
|
||||
assignScopeToResource: async (
|
||||
_parent: unknown,
|
||||
{ resourceId, scope }: { resourceId: string; scope: string },
|
||||
{ resourceId, name, description }: { resourceId: string; name: string; description?: string },
|
||||
context: GraphQLContext
|
||||
) => {
|
||||
return assignScopeToResource(resourceId, scope, context);
|
||||
return assignScopeToResource(resourceId, name, description ?? null, context);
|
||||
},
|
||||
removeScopeFromResource: async (
|
||||
_parent: unknown,
|
||||
|
||||
@ -101,3 +101,5 @@ export async function deleteResource(id: string, context: GraphQLContext): Promi
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type { Resource };
|
||||
|
||||
@ -27,7 +27,7 @@ export async function createResourceScope(
|
||||
name: string,
|
||||
description: string | null,
|
||||
context: GraphQLContext
|
||||
): Promise<any> {
|
||||
): Promise<ResourceScope> {
|
||||
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||
try {
|
||||
// LOG AVANT REQUETE
|
||||
@ -40,8 +40,13 @@ export async function createResourceScope(
|
||||
// LOG APRES REQUETE
|
||||
console.error("[DEBUG createResourceScope] Logto response:", response.data);
|
||||
return response.data;
|
||||
} catch (e: any) {
|
||||
console.error("[ERROR createResourceScope]", e?.response?.data || e);
|
||||
} catch (e: unknown) {
|
||||
if (typeof e === "object" && e !== null && "response" in e) {
|
||||
// @ts-expect-error: response might exist on error object
|
||||
console.error("[ERROR createResourceScope]", e.response?.data || e);
|
||||
} else {
|
||||
console.error("[ERROR createResourceScope]", e);
|
||||
}
|
||||
throw new GraphQLError("Erreur lors de la création de la permission de ressource", {
|
||||
extensions: { code: "CREATE_RESOURCE_SCOPE_FAILED" },
|
||||
});
|
||||
|
||||
@ -3,6 +3,17 @@ import { GraphQLError } from "graphql";
|
||||
import { GraphQLContext } from "../../context/types";
|
||||
import { getLogtoAccessToken } from "../User/resolver";
|
||||
|
||||
interface AxiosErrorResponse {
|
||||
response?: {
|
||||
data?: {
|
||||
code?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function getAccessTokenFromContext(context: GraphQLContext): string | null {
|
||||
return context.accessToken || null;
|
||||
}
|
||||
@ -14,7 +25,7 @@ export async function listPermissions(context: GraphQLContext) {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
throw new GraphQLError("Erreur lors de la récupération des permissions", {
|
||||
extensions: { code: "PERMISSIONS_FETCH_FAILED" },
|
||||
});
|
||||
@ -30,7 +41,7 @@ export async function createPermission(name: string, description: string | null,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
throw new GraphQLError("Erreur lors de la création de la permission", {
|
||||
extensions: { code: "CREATE_PERMISSION_FAILED" },
|
||||
});
|
||||
@ -44,7 +55,7 @@ export async function deletePermission(id: string, context: GraphQLContext) {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
throw new GraphQLError("Erreur lors de la suppression de la permission", {
|
||||
extensions: { code: "DELETE_PERMISSION_FAILED" },
|
||||
});
|
||||
@ -64,12 +75,23 @@ export async function assignPermissionToRole(roleId: string, permissionId: strin
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[DEBUG assignPermissionToRole] Logto response:", res.data);
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
// Si l'erreur est que la permission existe déjà, on considère comme succès
|
||||
if (e?.response?.data?.code === "role.scope_exists") {
|
||||
} catch (e: unknown) {
|
||||
const error = e as AxiosErrorResponse;
|
||||
if (
|
||||
typeof e === "object" &&
|
||||
e !== null &&
|
||||
"response" in error &&
|
||||
typeof error.response === "object" &&
|
||||
error.response !== null &&
|
||||
"data" in error.response &&
|
||||
typeof error.response.data === "object" &&
|
||||
error.response.data !== null &&
|
||||
"code" in error.response.data &&
|
||||
error.response.data.code === "role.scope_exists"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
console.error("[ERROR assignPermissionToRole]", e?.response?.data || e);
|
||||
console.error("[ERROR assignPermissionToRole]", error?.response?.data || e);
|
||||
throw new GraphQLError("Erreur lors de l'assignation du scope au rôle", {
|
||||
extensions: { code: "ASSIGN_SCOPE_TO_ROLE_FAILED" },
|
||||
});
|
||||
|
||||
@ -18,7 +18,7 @@ async function fetchRolePermissions(
|
||||
});
|
||||
// Les scopes sont des permissions
|
||||
return Array.isArray(response.data)
|
||||
? response.data.map((scope: any) => ({
|
||||
? response.data.map((scope: { id: string; name: string; description?: string }) => ({
|
||||
id: scope.id,
|
||||
name: scope.name,
|
||||
description: scope.description,
|
||||
@ -79,18 +79,25 @@ export async function assignRoleToUser(userId: string, roleId: string, context:
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
// Log détaillé côté serveur
|
||||
console.error("Erreur assignRoleToUser:", err?.response?.data || err);
|
||||
let message = "Erreur lors de l'assignation du rôle";
|
||||
if (err?.response?.data?.message) {
|
||||
message += `: ${err.response.data.message}`;
|
||||
} else if (err?.message) {
|
||||
message += `: ${err.message}`;
|
||||
if (axios.isAxiosError(err)) {
|
||||
console.error("Erreur assignRoleToUser:", err.response?.data || err);
|
||||
let message = "Erreur lors de l'assignation du rôle";
|
||||
if (err.response?.data?.message) {
|
||||
message += `: ${err.response.data.message}`;
|
||||
} else if (err.message) {
|
||||
message += `: ${err.message}`;
|
||||
}
|
||||
throw new GraphQLError(message, {
|
||||
extensions: { code: "ASSIGN_ROLE_FAILED", logto: err.response?.data },
|
||||
});
|
||||
} else {
|
||||
console.error("Erreur assignRoleToUser:", err);
|
||||
throw new GraphQLError("Erreur lors de l'assignation du rôle", {
|
||||
extensions: { code: "ASSIGN_ROLE_FAILED" },
|
||||
});
|
||||
}
|
||||
throw new GraphQLError(message, {
|
||||
extensions: { code: "ASSIGN_ROLE_FAILED", logto: err?.response?.data },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,3 +173,5 @@ export async function deleteRole(id: string, context: GraphQLContext) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type { Role };
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user