diff --git a/apps/api/app/services/blog_uploads.ts b/apps/api/app/services/blog_uploads.ts index fab92f7..7e518d4 100644 --- a/apps/api/app/services/blog_uploads.ts +++ b/apps/api/app/services/blog_uploads.ts @@ -59,8 +59,13 @@ export async function uploadBlogImage(file: MultipartFile): Promise sans avoir à connaître l'API host. APP_URL est + // posé par le ConfigMap k3s rubis-api-config ('https://app.rubis.pro'). + const apiHost = (process.env.APP_URL || 'http://localhost:3333').replace(/\/$/, '') + return { - publicPath: `/api/v1/uploads/blog/${filename}`, + publicPath: `${apiHost}/api/v1/uploads/blog/${filename}`, storageKey, contentType: extToContentType(ext), sizeBytes: file.size, diff --git a/apps/api/app/validators/post.ts b/apps/api/app/validators/post.ts index 80b51b4..ae60ddf 100644 --- a/apps/api/app/validators/post.ts +++ b/apps/api/app/validators/post.ts @@ -18,6 +18,20 @@ const title = () => vine.string().minLength(5).maxLength(80) const excerpt = () => vine.string().minLength(80).maxLength(280) const tags = () => vine.array(vine.string().maxLength(50)).maxLength(5).distinct() +/** + * URL d'image acceptée pour `hero_image_url` / `og_image_url` : + * - URL absolue HTTPS (image externe — rare, mais possible) + * - Path relatif commençant par /api/v1/uploads/... (uploads MinIO via + * notre endpoint `BlogUploadsController.show`) + * + * On refuse `http://` (sécurité mixed content) et autres chemins relatifs. + */ +const imagePathOrUrl = () => + vine + .string() + .maxLength(500) + .regex(/^(https:\/\/.+|\/api\/v1\/uploads\/.+)$/) + /** * Création d'un brouillon. On accepte `status` mais c'est toujours `draft` * à la création — la publication passe par /publish. @@ -30,9 +44,9 @@ export const createPostValidator = vine.compile( contentMd: vine.string().minLength(50), tags: tags().optional(), authorName: vine.string().minLength(2).maxLength(100).optional(), - heroImageUrl: vine.string().url().nullable().optional(), + heroImageUrl: imagePathOrUrl().nullable().optional(), heroImageAlt: vine.string().maxLength(250).nullable().optional(), - ogImageUrl: vine.string().url().nullable().optional(), + ogImageUrl: imagePathOrUrl().nullable().optional(), canonicalUrl: vine.string().url().nullable().optional(), noindex: vine.boolean().optional(), }), @@ -50,9 +64,9 @@ export const updatePostValidator = vine.compile( contentMd: vine.string().minLength(50).optional(), tags: tags().optional(), authorName: vine.string().minLength(2).maxLength(100).optional(), - heroImageUrl: vine.string().url().nullable().optional(), + heroImageUrl: imagePathOrUrl().nullable().optional(), heroImageAlt: vine.string().maxLength(250).nullable().optional(), - ogImageUrl: vine.string().url().nullable().optional(), + ogImageUrl: imagePathOrUrl().nullable().optional(), canonicalUrl: vine.string().url().nullable().optional(), noindex: vine.boolean().optional(), }),