Compare commits

...

3 Commits

Author SHA1 Message Date
Corentin Thomasset
9b5f3993c3 chore(release): update versions (#518)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-30 11:51:10 +02:00
Corentin Thomasset
b28772317c fix(file-upload): set default parameter charset to utf8 (#521) 2025-09-29 21:20:43 +02:00
Corentin Thomasset
a3f9f05c66 feat(organizations): restrict organization deletion to owners only (#517) 2025-09-26 01:49:59 +02:00
17 changed files with 106 additions and 6 deletions

View File

@@ -1,5 +1,11 @@
# @papra/app-client
## 0.9.5
### Patch Changes
- [#517](https://github.com/papra-hq/papra/pull/517) [`a3f9f05`](https://github.com/papra-hq/papra/commit/a3f9f05c664b4995b62db59f2e9eda8a3bfef0de) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Prevented organization deletion by non-organization owner
## 0.9.4
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@papra/app-client",
"type": "module",
"version": "0.9.4",
"version": "0.9.5",
"private": true,
"packageManager": "pnpm@10.12.3",
"description": "Papra frontend client",

View File

@@ -143,6 +143,7 @@ export const translations: Partial<TranslationsDictionary> = {
'organization.settings.delete.confirm.confirm-button': 'Organisation löschen',
'organization.settings.delete.confirm.cancel-button': 'Abbrechen',
'organization.settings.delete.success': 'Organisation gelöscht',
'organization.settings.delete.only-owner': 'Nur der Organisationsinhaber kann diese Organisation löschen.',
'organizations.members.title': 'Mitglieder',
'organizations.members.description': 'Verwalten Sie Ihre Organisationsmitglieder',

View File

@@ -141,6 +141,7 @@ export const translations = {
'organization.settings.delete.confirm.confirm-button': 'Delete organization',
'organization.settings.delete.confirm.cancel-button': 'Cancel',
'organization.settings.delete.success': 'Organization deleted',
'organization.settings.delete.only-owner': 'Only the organization owner can delete this organization.',
'organizations.members.title': 'Members',
'organizations.members.description': 'Manage your organization members',

View File

@@ -143,6 +143,7 @@ export const translations: Partial<TranslationsDictionary> = {
'organization.settings.delete.confirm.confirm-button': 'Eliminar organización',
'organization.settings.delete.confirm.cancel-button': 'Cancelar',
'organization.settings.delete.success': 'Organización eliminada',
'organization.settings.delete.only-owner': 'Solo el propietario de la organización puede eliminar esta organización.',
'organizations.members.title': 'Miembros',
'organizations.members.description': 'Administra los miembros de tu organización',

View File

@@ -143,6 +143,7 @@ export const translations: Partial<TranslationsDictionary> = {
'organization.settings.delete.confirm.confirm-button': 'Supprimer l\'organisation',
'organization.settings.delete.confirm.cancel-button': 'Annuler',
'organization.settings.delete.success': 'Organisation supprimée',
'organization.settings.delete.only-owner': 'Seul le propriétaire de l\'organisation peut supprimer cette organisation.',
'organizations.members.title': 'Membres',
'organizations.members.description': 'Gérez les membres de votre organisation.',

View File

@@ -143,6 +143,7 @@ export const translations: Partial<TranslationsDictionary> = {
'organization.settings.delete.confirm.confirm-button': 'Elimina organizzazione',
'organization.settings.delete.confirm.cancel-button': 'Annulla',
'organization.settings.delete.success': 'Organizzazione eliminata',
'organization.settings.delete.only-owner': 'Solo il proprietario dell\'organizzazione può eliminare questa organizzazione.',
'organizations.members.title': 'Membri',
'organizations.members.description': 'Gestisci i membri della tua organizzazione',

View File

@@ -143,6 +143,7 @@ export const translations: Partial<TranslationsDictionary> = {
'organization.settings.delete.confirm.confirm-button': 'Usuń organizację',
'organization.settings.delete.confirm.cancel-button': 'Anuluj',
'organization.settings.delete.success': 'Organizacja została usunięta',
'organization.settings.delete.only-owner': 'Tylko właściciel organizacji może usunąć tę organizację.',
'organizations.members.title': 'Członkowie',
'organizations.members.description': 'Zarządzaj członkami swojej organizacji',

View File

@@ -143,6 +143,7 @@ export const translations: Partial<TranslationsDictionary> = {
'organization.settings.delete.confirm.confirm-button': 'Excluir organização',
'organization.settings.delete.confirm.cancel-button': 'Cancelar',
'organization.settings.delete.success': 'Organização excluída',
'organization.settings.delete.only-owner': 'Apenas o proprietário da organização pode excluir esta organização.',
'organizations.members.title': 'Membros',
'organizations.members.description': 'Gerencie os membros da sua organização',

View File

@@ -143,6 +143,7 @@ export const translations: Partial<TranslationsDictionary> = {
'organization.settings.delete.confirm.confirm-button': 'Eliminar organização',
'organization.settings.delete.confirm.cancel-button': 'Cancelar',
'organization.settings.delete.success': 'Organização eliminada',
'organization.settings.delete.only-owner': 'Apenas o proprietário da organização pode eliminar esta organização.',
'organizations.members.title': 'Membros',
'organizations.members.description': 'Gira os membros da sua organização',

View File

@@ -143,6 +143,7 @@ export const translations: Partial<TranslationsDictionary> = {
'organization.settings.delete.confirm.confirm-button': 'Șterge organizație',
'organization.settings.delete.confirm.cancel-button': 'Anulează',
'organization.settings.delete.success': 'Organizație ștearsă cu succes',
'organization.settings.delete.only-owner': 'Doar proprietarul organizației poate șterge această organizație.',
'organizations.members.title': 'Membri',
'organizations.members.description': 'Gestionează membrii organizației tale',

View File

@@ -15,7 +15,7 @@ import { Button } from '@/modules/ui/components/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/modules/ui/components/card';
import { createToast } from '@/modules/ui/components/sonner';
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
import { useDeleteOrganization, useUpdateOrganization } from '../organizations.composables';
import { useCurrentUserRole, useDeleteOrganization, useUpdateOrganization } from '../organizations.composables';
import { organizationNameSchema } from '../organizations.schemas';
import { fetchOrganization } from '../organizations.services';
@@ -24,6 +24,8 @@ const DeleteOrganizationCard: Component<{ organization: Organization }> = (props
const { confirm } = useConfirmModal();
const { t } = useI18n();
const { getIsOwner, query } = useCurrentUserRole({ organizationId: props.organization.id });
const handleDelete = async () => {
const confirmed = await confirm({
title: t('organization.settings.delete.confirm.title'),
@@ -54,10 +56,16 @@ const DeleteOrganizationCard: Component<{ organization: Organization }> = (props
</CardDescription>
</CardHeader>
<CardFooter class="pt-6">
<Button onClick={handleDelete} variant="destructive">
<CardFooter class="pt-6 gap-4">
<Button onClick={handleDelete} variant="destructive" disabled={!getIsOwner()}>
{t('organization.settings.delete.confirm.confirm-button')}
</Button>
<Show when={query.isSuccess && !getIsOwner()}>
<span class="text-sm text-muted-foreground">
{t('organization.settings.delete.only-owner')}
</span>
</Show>
</CardFooter>
</Card>
</div>

View File

@@ -1,5 +1,13 @@
# @papra/app-server
## 0.9.5
### Patch Changes
- [#521](https://github.com/papra-hq/papra/pull/521) [`b287723`](https://github.com/papra-hq/papra/commit/b28772317c3662555e598755b85597d6cd5aeea1) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Properly handle file names encoding (utf8 instead of latin1) to support non-ASCII characters.
- [#517](https://github.com/papra-hq/papra/pull/517) [`a3f9f05`](https://github.com/papra-hq/papra/commit/a3f9f05c664b4995b62db59f2e9eda8a3bfef0de) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Prevented organization deletion by non-organization owner
## 0.9.4
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@papra/app-server",
"type": "module",
"version": "0.9.4",
"version": "0.9.5",
"private": true,
"packageManager": "pnpm@10.12.3",
"description": "Papra app server",

View File

@@ -127,5 +127,71 @@ describe('documents e2e', () => {
// Ensure no file is saved in the storage
expect(documentsStorageService._getStorage().size).to.eql(0);
});
// https://github.com/papra-hq/papra/issues/519
test('uploading documents with various UTF-8 characters in filenames', async () => {
const { db } = await createInMemoryDatabase({
users: [{ id: 'usr_111111111111111111111111', email: 'user@example.com' }],
organizations: [{ id: 'org_222222222222222222222222', name: 'Org 1' }],
organizationMembers: [{ organizationId: 'org_222222222222222222222222', userId: 'usr_111111111111111111111111', role: ORGANIZATION_ROLES.OWNER }],
});
const { app } = await createServer({
db,
config: overrideConfig({
env: 'test',
documentsStorage: {
driver: 'in-memory',
},
}),
});
// Various UTF-8 characters that cause encoding issues
const testCases = [
{ filename: 'ΒΕΒΑΙΩΣΗ ΧΑΡΕΣ.txt', content: 'Filename with Greek characters' },
{ filename: 'résumé français.txt', content: 'French document' },
{ filename: 'documento español.txt', content: 'Spanish document' },
{ filename: '日本語ファイル.txt', content: 'Japanese document' },
{ filename: 'файл на русском.txt', content: 'Russian document' },
{ filename: 'émojis 🎉📄.txt', content: 'Document with emojis' },
];
for (const testCase of testCases) {
const formData = new FormData();
formData.append('file', new File([testCase.content], testCase.filename, { type: 'text/plain' }));
const body = new Response(formData);
const createDocumentResponse = await app.request(
'/api/organizations/org_222222222222222222222222/documents',
{
method: 'POST',
headers: {
...Object.fromEntries(body.headers.entries()),
},
body: await body.arrayBuffer(),
},
{ loggedInUserId: 'usr_111111111111111111111111' },
);
expect(createDocumentResponse.status).to.eql(200);
const { document } = (await createDocumentResponse.json()) as { document: Document };
// Each filename should be preserved correctly
expect(document.name).to.eql(testCase.filename);
expect(document.originalName).to.eql(testCase.filename);
// Retrieve the document
const getDocumentResponse = await app.request(
`/api/organizations/org_222222222222222222222222/documents/${document.id}`,
{ method: 'GET' },
{ loggedInUserId: 'usr_111111111111111111111111' },
);
expect(getDocumentResponse.status).to.eql(200);
const { document: retrievedDocument } = (await getDocumentResponse.json()) as { document: Document };
expect(retrievedDocument).to.eql({ ...document, tags: [] });
}
});
});
});

View File

@@ -8,7 +8,7 @@ import { createUsersRepository } from '../users/users.repository';
import { memberIdSchema, organizationIdSchema } from './organization.schemas';
import { ORGANIZATION_ROLES } from './organizations.constants';
import { createOrganizationsRepository } from './organizations.repository';
import { checkIfUserCanCreateNewOrganization, createOrganization, ensureUserIsInOrganization, inviteMemberToOrganization, removeMemberFromOrganization, updateOrganizationMemberRole } from './organizations.usecases';
import { checkIfUserCanCreateNewOrganization, createOrganization, ensureUserIsInOrganization, ensureUserIsOwnerOfOrganization, inviteMemberToOrganization, removeMemberFromOrganization, updateOrganizationMemberRole } from './organizations.usecases';
export function registerOrganizationsRoutes(context: RouteDefinitionContext) {
setupGetOrganizationsRoute(context);
@@ -130,7 +130,9 @@ function setupDeleteOrganizationRoute({ app, db }: RouteDefinitionContext) {
const organizationsRepository = createOrganizationsRepository({ db });
// No Promise.all as we want to ensure consistency in error handling
await ensureUserIsInOrganization({ userId, organizationId, organizationsRepository });
await ensureUserIsOwnerOfOrganization({ userId, organizationId, organizationsRepository });
await organizationsRepository.deleteOrganization({ organizationId });

View File

@@ -61,6 +61,7 @@ export async function getFileStreamFromMultipartForm({
files: 1, // Only allow one file
fileSize: maxFileSize,
},
defParamCharset: 'utf8',
})
.on('file', (formFieldname, fileStream, info) => {
if (formFieldname !== fieldName) {