Skip to content
Merged

Fix #41

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions api/migrations/202605041327.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Pool } from "mariadb/*";
import { MigrationParams } from "umzug";

/**
* Migration pour ajouter une colonne "mockup_embed" à la table "project".
*/

export async function up({ context: pool }: MigrationParams<Pool>) {
const conn = await pool.getConnection();
try {
await conn.query(
`ALTER TABLE IF EXISTS project ADD COLUMN IF NOT EXISTS pined BOOLEAN;`,
);
} finally {
conn.release();
}
}

export async function down({ context: pool }: MigrationParams<Pool>) {
const conn = await pool.getConnection();
try {
await conn.query(
`ALTER TABLE IF EXISTS project DROP COLUMN IF EXISTS pined;`,
);
} finally {
conn.release();
}
}
29 changes: 29 additions & 0 deletions api/migrations/202605041407.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Pool } from "mariadb/*";
import { MigrationParams } from "umzug";

/**
* Migration pour définir une valeur par défaut vide sur la colonne "section"
* de la table "project_stack" tout en la conservant NOT NULL.
*/

export async function up({ context: pool }: MigrationParams<Pool>) {
const conn = await pool.getConnection();
try {
await conn.query(
`ALTER TABLE IF EXISTS project_stack MODIFY COLUMN IF EXISTS section VARCHAR(255) NOT NULL DEFAULT '';`,
);
} finally {
conn.release();
}
}

export async function down({ context: pool }: MigrationParams<Pool>) {
const conn = await pool.getConnection();
try {
await conn.query(
`ALTER TABLE IF EXISTS project_stack MODIFY COLUMN IF EXISTS section VARCHAR(255) NOT NULL;`,
);
} finally {
conn.release();
}
}
22 changes: 15 additions & 7 deletions api/src/graphql/resolvers/projectResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@ function sanitizeProjectInput(input: Partial<Omit<Project, "id">>): void {
throw new Error("Invalid website URL");
}
if (input.website.label)
input.website.label = sanitizeString(input.website.label);
input.website.label = sanitizeString(input.website.label, {
preserveEntities: true,
});
}
if (input.mockup) {
if (input.mockup.url) {
if (!isValidUrl(input.mockup.url)) throw new Error("Invalid mockup URL");
}
if (input.mockup.label)
input.mockup.label = sanitizeString(input.mockup.label);
input.mockup.label = sanitizeString(input.mockup.label, {
preserveEntities: true,
});
if (input.mockup.images) {
for (const [i, image] of input.mockup.images.entries()) {
if (!validator.isUUID(image.id)) delete input.mockup.images[i];
Expand All @@ -61,15 +65,18 @@ function sanitizeProjectInput(input: Partial<Omit<Project, "id">>): void {
}
if (input.client) {
if (input.client.label)
input.client.label = sanitizeString(input.client.label);
input.client.label = sanitizeString(input.client.label, {
preserveEntities: true,
});
if (input.client.logo && !validator.isUUID(input.client.logo as string))
delete input.client.logo;
}
if (input.manager) {
if (input.manager.name)
input.manager.name = sanitizeString(input.manager.name);
input.manager.name = sanitizeString(input.manager.name, {
preserveEntities: true,
});
if (input.manager.email) {
input.manager.email = sanitizeString(input.manager.email);
if (!validator.isEmail(input.manager.email))
throw new Error("Invalid manager email");
}
Expand Down Expand Up @@ -208,7 +215,7 @@ const projectResolver = {
checkAuth(context);
const input = { ..._args.input };
if (isEmpty(input.label)) throw new Error("Label is required");
input.label = sanitizeString(input.label);
input.label = sanitizeString(input.label, { preserveEntities: true });
sanitizeProjectInput(input);
if (isEmpty(input.slug)) input.slug = slugify(input.label);
const result = await context.projectRepo.create(input);
Expand All @@ -233,7 +240,8 @@ const projectResolver = {
validateId(_args.id);

const input = { ..._args.input, id: _args.id };
if (input.label) input.label = sanitizeString(input.label);
if (input.label)
input.label = sanitizeString(input.label, { preserveEntities: true });
sanitizeProjectInput(input);
const result = await context.projectRepo.update(_args.id, input);
if (!result) throw new Error("Failed to update project");
Expand Down
4 changes: 2 additions & 2 deletions api/src/graphql/resolvers/stackResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ function sanitizeStackInput(input: Partial<Omit<Stack, "id">>) {
if (input.label) input.label = sanitizeString(input.label);
if (input.icon && !validator.isUUID(input.icon as string)) delete input.icon;
if (input.description) input.description = sanitizeString(input.description);
if (input.versions) input.versions = input.versions.map(sanitizeString);
if (input.skills) input.skills = input.skills.map(sanitizeString);
if (input.versions) input.versions = input.versions.map((v) => sanitizeString(v));
if (input.skills) input.skills = input.skills.map((s) => sanitizeString(s));
if (input.category && !validator.isUUID(input.category as string))
delete input.category;
}
Expand Down
2 changes: 2 additions & 0 deletions api/src/graphql/schemas/projectSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const projectTypes = `
label: String!
thumbnail: ID
categories: [ID!]
pined: Boolean
website: Website
mockup: Mockup
client: Client
Expand Down Expand Up @@ -171,6 +172,7 @@ export const projectInputs = `
label: String!
thumbnail: ID
categories: [ID!]
pined: Boolean
website: WebsiteInput
mockup: MockupInput
client: ClientInput
Expand Down
11 changes: 7 additions & 4 deletions api/src/repositories/ProjectRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export default class ProjectRepository extends BaseRepository {
slug: projectRow.slug,
label: projectRow.label,
thumbnail: projectRow.thumbnail_id,
pined: projectRow.pined,
startDate: projectRow.start_date,
endDate: projectRow.end_date,
categories: relations.categories,
Expand Down Expand Up @@ -260,13 +261,14 @@ export default class ProjectRepository extends BaseRepository {
// Insère le projet principal
await conn.query(
`INSERT INTO project (
id, slug, label, thumbnail_id, website_url, website_label, mockup_url, mockup_label, mockup_embed, client_label, client_logo_id, manager_name, manager_email, start_date, end_date, intro, presentation_context, presentation_client, presentation_issue, presentation_audience, need_features, need_functional_constraints, need_technical_constraints, organization_workload, organization_anticipation, organization_methodology, organization_evolution, organization_validation, feedback, feedback_client, kpis_issues, kpis_points, kpis_commits, kpis_pull_requests
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, slug, label, thumbnail_id, pined, website_url, website_label, mockup_url, mockup_label, mockup_embed, client_label, client_logo_id, manager_name, manager_email, start_date, end_date, intro, presentation_context, presentation_client, presentation_issue, presentation_audience, need_features, need_functional_constraints, need_technical_constraints, organization_workload, organization_anticipation, organization_methodology, organization_evolution, organization_validation, feedback, feedback_client, kpis_issues, kpis_points, kpis_commits, kpis_pull_requests
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
id,
project.slug,
project.label,
project.thumbnail || null,
project.pined || false,
project.website?.url || null,
project.website?.label || null,
project.mockup?.url || null,
Expand Down Expand Up @@ -341,7 +343,7 @@ export default class ProjectRepository extends BaseRepository {
id,
stack_id,
version ?? null,
section ?? null,
section ?? "",
]),
);
}
Expand All @@ -366,6 +368,7 @@ export default class ProjectRepository extends BaseRepository {
slug: project.slug,
label: project.label,
thumbnail_id: project.thumbnail,
pined: project.pined,
website_url: project.website?.url,
website_label: project.website?.label,
mockup_url: project.mockup?.url,
Expand Down Expand Up @@ -543,7 +546,7 @@ export default class ProjectRepository extends BaseRepository {
const input: StackRow[] = stacks.map((s) => ({
stack_id: s.id,
version: s.version ?? null,
section: s.section ?? null,
section: s.section ?? "",
}));
const toAdd = input.filter(
(s) =>
Expand Down
2 changes: 2 additions & 0 deletions api/src/types/projectTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface Project {
label: string;
thumbnail?: string;
categories?: string[];
pined?: boolean;
website?: {
url: string;
label?: string;
Expand Down Expand Up @@ -65,6 +66,7 @@ export interface ProjectRow {
slug: string;
label: string;
thumbnail_id?: string;
pined?: boolean;
website_url?: string;
website_label?: string;
mockup_url?: string;
Expand Down
30 changes: 28 additions & 2 deletions api/src/utils/stringUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,36 @@ export function slugify(text: string): string {
/**
* Nettoie une chaîne pour éviter les injections XSS
* @param {string} str La chaîne à nettoyer
* @param {{ preserveEntities?: boolean }} [options] Options de nettoyage
* @return {string} La chaîne nettoyée
*/
export function sanitizeString(str: string): string {
return validator.escape(str.trim());
export function sanitizeString(
str: string,
options?: { preserveEntities?: boolean },
): string {
const trimmed = str.trim();

if (!options?.preserveEntities) {
return validator.escape(trimmed);
}

const preservedEntities = ["&shy;", "&#8203;", "&nbsp;"];
const placeholders = new Map<string, string>();
let protectedValue = trimmed;

preservedEntities.forEach((entity, index) => {
const token = `__ENTITY_${index}__`;
const re = new RegExp(entity.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
protectedValue = protectedValue.replace(re, token);
placeholders.set(token, entity);
});

let escaped = validator.escape(protectedValue);
placeholders.forEach((entity, token) => {
escaped = escaped.replaceAll(token, entity);
});

return escaped;
}

/**
Expand Down
Loading