mirror of
https://github.com/papra-hq/papra.git
synced 2025-12-17 03:51:45 -06:00
Compare commits
3 Commits
@papra/app
...
@papra/app
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b5f3993c3 | ||
|
|
b28772317c | ||
|
|
a3f9f05c66 |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: [] });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user