Skip to content
This repository was archived by the owner on Jun 29, 2025. It is now read-only.

feat(share): add share ID length setting #677

Merged
Merged
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
5 changes: 5 additions & 0 deletions backend/prisma/seed/config.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ const configVariables: ConfigVariables = {
defaultValue: "0",
secret: false,
},
shareIdLength: {
type: "number",
defaultValue: "8",
secret: false,
},
maxSize: {
type: "number",
defaultValue: "1000000000",
Expand Down
46 changes: 33 additions & 13 deletions frontend/src/components/upload/modals/showCreateUploadModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const showCreateUploadModal = (
allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean;
maxExpirationInHours: number;
shareIdLength: number;
simplified: boolean;
},
files: FileUpload[],
Expand Down Expand Up @@ -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<string> => {
const generateAvailableLink = async (
shareIdLength: number,
times: number = 10,
): Promise<string> => {
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;
}
Expand All @@ -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);

Expand Down Expand Up @@ -229,7 +241,12 @@ const CreateUploadModalBody = ({
<Button
style={{ flex: "0 0 auto" }}
variant="outline"
onClick={() => form.setFieldValue("link", generateLink())}
onClick={() =>
form.setFieldValue(
"link",
generateShareId(options.shareIdLength),
)
}
>
<FormattedMessage id="common.button.generate" />
</Button>
Expand Down Expand Up @@ -461,6 +478,7 @@ const SimplifiedCreateUploadModalModal = ({
allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean;
maxExpirationInHours: number;
shareIdLength: number;
};
}) => {
const modals = useModals();
Expand All @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/i18n/translations/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/i18n/translations/fr-FR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/upload/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ const Upload = ({
),
enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
maxExpirationInHours: config.get("share.maxExpiration"),
shareIdLength: config.get("share.shareIdLength"),
simplified,
},
files,
Expand Down
Loading