diff --git a/backend/prisma/seed/config.seed.ts b/backend/prisma/seed/config.seed.ts index a203143..26a39a3 100644 --- a/backend/prisma/seed/config.seed.ts +++ b/backend/prisma/seed/config.seed.ts @@ -51,6 +51,11 @@ const configVariables: ConfigVariables = { defaultValue: "0", secret: false, }, + shareIdLength: { + type: "number", + defaultValue: "8", + secret: false, + }, maxSize: { type: "number", defaultValue: "1000000000", diff --git a/frontend/src/components/upload/modals/showCreateUploadModal.tsx b/frontend/src/components/upload/modals/showCreateUploadModal.tsx index fe5ab71..1345716 100644 --- a/frontend/src/components/upload/modals/showCreateUploadModal.tsx +++ b/frontend/src/components/upload/modals/showCreateUploadModal.tsx @@ -40,6 +40,7 @@ const showCreateUploadModal = ( allowUnauthenticatedShares: boolean; enableEmailRecepients: boolean; maxExpirationInHours: number; + shareIdLength: number; simplified: boolean; }, files: FileUpload[], @@ -72,18 +73,28 @@ const showCreateUploadModal = ( }); }; -const generateLink = () => - Buffer.from(Math.random().toString(), "utf8") - .toString("base64") - .substring(10, 17); +const generateShareId = (length: number = 16) => { + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + const randomArray = new Uint8Array(length >= 3 ? length : 3); + crypto.getRandomValues(randomArray); + randomArray.forEach((number) => { + result += chars[number % chars.length]; + }); + return result; +}; -const generateAvailableLink = async (times = 10): Promise => { +const generateAvailableLink = async ( + shareIdLength: number, + times: number = 10, +): Promise => { if (times <= 0) { throw new Error("Could not generate available link"); } - const _link = generateLink(); + const _link = generateShareId(shareIdLength); if (!(await shareService.isShareIdAvailable(_link))) { - return await generateAvailableLink(times - 1); + return await generateAvailableLink(shareIdLength, times - 1); } else { return _link; } @@ -102,12 +113,13 @@ const CreateUploadModalBody = ({ allowUnauthenticatedShares: boolean; enableEmailRecepients: boolean; maxExpirationInHours: number; + shareIdLength: number; }; }) => { const modals = useModals(); const t = useTranslate(); - const generatedLink = generateLink(); + const generatedLink = generateShareId(options.shareIdLength); const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true); @@ -229,7 +241,12 @@ const CreateUploadModalBody = ({ @@ -461,6 +478,7 @@ const SimplifiedCreateUploadModalModal = ({ allowUnauthenticatedShares: boolean; enableEmailRecepients: boolean; maxExpirationInHours: number; + shareIdLength: number; }; }) => { const modals = useModals(); @@ -485,10 +503,12 @@ const SimplifiedCreateUploadModalModal = ({ }); const onSubmit = form.onSubmit(async (values) => { - const link = await generateAvailableLink().catch(() => { - toast.error(t("upload.modal.link.error.taken")); - return undefined; - }); + const link = await generateAvailableLink(options.shareIdLength).catch( + () => { + toast.error(t("upload.modal.link.error.taken")); + return undefined; + }, + ); if (!link) { return; diff --git a/frontend/src/i18n/translations/en-US.ts b/frontend/src/i18n/translations/en-US.ts index f44a399..bc7292b 100644 --- a/frontend/src/i18n/translations/en-US.ts +++ b/frontend/src/i18n/translations/en-US.ts @@ -468,6 +468,9 @@ export default { "admin.config.share.max-expiration": "Max expiration", "admin.config.share.max-expiration.description": "Maximum share expiration in hours. Set to 0 to allow unlimited expiration.", + "admin.config.share.share-id-length": "Default share ID length", + "admin.config.share.share-id-length.description": + "Default length for the generated ID of a share. This value is also used to generate links for reverse shares. A value below 8 is not considered secure.", "admin.config.share.max-size": "Max size", "admin.config.share.max-size.description": "Maximum share size in bytes", "admin.config.share.zip-compression-level": "Zip compression level", diff --git a/frontend/src/i18n/translations/fr-FR.ts b/frontend/src/i18n/translations/fr-FR.ts index 7300398..9c06134 100644 --- a/frontend/src/i18n/translations/fr-FR.ts +++ b/frontend/src/i18n/translations/fr-FR.ts @@ -336,6 +336,9 @@ export default { "admin.config.share.allow-unauthenticated-shares.description": "Permet aux visiteurs de créer des partages", "admin.config.share.max-expiration": "Échéance", "admin.config.share.max-expiration.description": "Échéance du partage en heures. Indiquez 0 pour qu’il n’expire jamais.", + "admin.config.share.share-id-length": "Taille de l'identifiant généré", + "admin.config.share.share-id-length.description": + "Taille par défaut de l'identifiant généré pour un partage. Cette valeur est aussi utilisée pour générer les liens des partages inverses. Une valeur inférieure à 8 n'est pas considérée sûre.", "admin.config.share.max-size": "Taille max", "admin.config.share.max-size.description": "Taille maximale du partage en octets", "admin.config.share.zip-compression-level": "Niveau de compression", diff --git a/frontend/src/pages/upload/index.tsx b/frontend/src/pages/upload/index.tsx index a611ae8..5c094c5 100644 --- a/frontend/src/pages/upload/index.tsx +++ b/frontend/src/pages/upload/index.tsx @@ -140,6 +140,7 @@ const Upload = ({ ), enableEmailRecepients: config.get("email.enableShareEmailRecipients"), maxExpirationInHours: config.get("share.maxExpiration"), + shareIdLength: config.get("share.shareIdLength"), simplified, }, files,