feat(mail): E-Mails mit Leserecht als ignoriert markieren
Der Button „Als ignoriert markieren“ war für alle mit VIEW_MAIL sichtbar, der Aufruf (PATCH :id/status) verlangte aber MANAGE_ALL und scheiterte mit 403. Neuer Endpunkt POST /api/emails/:id/ignore mit VIEW_MAIL, der nur Status 3 setzen kann; bereits verarbeitete E-Mails werden abgelehnt. Das allgemeine Statusändern bleibt Admins vorbehalten. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f97177943d
commit
3cea3fb239
4 files changed
+85
-2
No files matched your search
@@ -7,6 +7,9 @@ import { Attachment } from '../database/entities/attachment.entity';
|
|||||||
import { Content } from '../database/entities/content.entity';
|
import { Content } from '../database/entities/content.entity';
|
||||||
import { PaperlessService } from '../paperless/paperless.service';
|
import { PaperlessService } from '../paperless/paperless.service';
|
||||||
import { ImapFolderService } from './imap-folder.service';
|
import { ImapFolderService } from './imap-folder.service';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { PERMISSIONS_KEY } from '../auth/permissions.decorator';
|
||||||
|
import { Permission } from '../auth/permissions.enum';
|
||||||
|
|
||||||
const mockEmails: Partial<Email>[] = [
|
const mockEmails: Partial<Email>[] = [
|
||||||
{
|
{
|
||||||
@@ -173,6 +176,47 @@ describe('EmailController', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ignore', () => {
|
||||||
|
it.each([
|
||||||
|
['Neu', 0],
|
||||||
|
['Fehler', 2],
|
||||||
|
['zur Nachbearbeitung', 4],
|
||||||
|
])(
|
||||||
|
'markiert eine E-Mail mit Status %s als ignoriert',
|
||||||
|
async (_l, status) => {
|
||||||
|
emailRepo.findOneOrFail.mockResolvedValue({ Id: 1, Status: status });
|
||||||
|
|
||||||
|
const result = await controller.ignore('1', { user: {} });
|
||||||
|
|
||||||
|
expect(emailRepo.save).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ Id: 1, Status: 3 }),
|
||||||
|
);
|
||||||
|
expect(result).toEqual(expect.objectContaining({ Status: 3 }));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('lehnt eine bereits verarbeitete E-Mail ab', async () => {
|
||||||
|
emailRepo.findOneOrFail.mockResolvedValue({ Id: 1, Status: 1 });
|
||||||
|
|
||||||
|
await expect(controller.ignore('1', { user: {} })).rejects.toThrow(
|
||||||
|
BadRequestException,
|
||||||
|
);
|
||||||
|
expect(emailRepo.save).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ist für alle mit VIEW_MAIL freigegeben', () => {
|
||||||
|
const handler = Object.getOwnPropertyDescriptor(
|
||||||
|
EmailController.prototype,
|
||||||
|
'ignore',
|
||||||
|
)?.value as () => unknown;
|
||||||
|
const permissions = new Reflector().get<Permission[]>(
|
||||||
|
PERMISSIONS_KEY,
|
||||||
|
handler,
|
||||||
|
);
|
||||||
|
expect(permissions).toEqual([Permission.VIEW_MAIL]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('updateStatus speichert den neuen Status', async () => {
|
it('updateStatus speichert den neuen Status', async () => {
|
||||||
await controller.updateStatus('1', 2);
|
await controller.updateStatus('1', 2);
|
||||||
expect(emailRepo.save).toHaveBeenCalledWith(
|
expect(emailRepo.save).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import { Permission } from '../auth/permissions.enum';
|
|||||||
* würde sie sonst sofort wieder auf "Verarbeitet" setzen.
|
* würde sie sonst sofort wieder auf "Verarbeitet" setzen.
|
||||||
*/
|
*/
|
||||||
const STATUS_NEU = 0;
|
const STATUS_NEU = 0;
|
||||||
|
const STATUS_VERARBEITET = 1;
|
||||||
|
const STATUS_IGNORIERT = 3;
|
||||||
const STATUS_NACHBEARBEITUNG = 4;
|
const STATUS_NACHBEARBEITUNG = 4;
|
||||||
|
|
||||||
@Controller('api/emails')
|
@Controller('api/emails')
|
||||||
@@ -123,6 +125,39 @@ export class EmailController {
|
|||||||
return { message: 'Status aktualisiert' };
|
return { message: 'Status aktualisiert' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markiert eine E-Mail als ignoriert. Anders als `PATCH :id/status` darf das
|
||||||
|
* jeder mit Zugriff aufs Mailpostfach – der Endpunkt kann aber nur Status 3
|
||||||
|
* setzen. Bereits verarbeitete E-Mails bleiben unangetastet.
|
||||||
|
*/
|
||||||
|
@Post(':id/ignore')
|
||||||
|
@RequirePermissions(Permission.VIEW_MAIL)
|
||||||
|
async ignore(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Request() req: { user?: { email?: string; userId?: string } },
|
||||||
|
) {
|
||||||
|
const email = await this.emailRepo.findOneOrFail({
|
||||||
|
where: { Id: parseInt(id, 10) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (email.Status === STATUS_VERARBEITET) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Eine bereits verarbeitete E-Mail kann nicht ignoriert werden.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vorherigerStatus = email.Status;
|
||||||
|
email.Status = STATUS_IGNORIERT;
|
||||||
|
await this.emailRepo.save(email);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`E-Mail ${id} von Status ${vorherigerStatus} als ignoriert markiert ` +
|
||||||
|
`(durch ${req.user?.email ?? req.user?.userId ?? 'unbekannt'})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
@Post(':id/reimport')
|
@Post(':id/reimport')
|
||||||
@RequirePermissions(Permission.MANAGE_ALL)
|
@RequirePermissions(Permission.MANAGE_ALL)
|
||||||
async reimport(
|
async reimport(
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export const emailsApi = {
|
|||||||
updateStatus: (id: number, status: number) =>
|
updateStatus: (id: number, status: number) =>
|
||||||
api.patch<{ message?: string }>(`/api/emails/${id}/status`, { status }).then((r) => r.data),
|
api.patch<{ message?: string }>(`/api/emails/${id}/status`, { status }).then((r) => r.data),
|
||||||
|
|
||||||
|
// Markiert eine E-Mail als ignoriert (für alle mit Zugriff aufs Mailpostfach).
|
||||||
|
ignore: (id: number) =>
|
||||||
|
api.post<EmailItem>(`/api/emails/${id}/ignore`).then((r) => r.data),
|
||||||
|
|
||||||
// Gibt eine bereits abgeschlossene E-Mail wieder zur Bearbeitung frei (nur Admins).
|
// Gibt eine bereits abgeschlossene E-Mail wieder zur Bearbeitung frei (nur Admins).
|
||||||
reimport: (id: number) =>
|
reimport: (id: number) =>
|
||||||
api.post<EmailItem>(`/api/emails/${id}/reimport`).then((r) => r.data),
|
api.post<EmailItem>(`/api/emails/${id}/reimport`).then((r) => r.data),
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ export default function MailDetailPage() {
|
|||||||
const handleIgnore = async () => {
|
const handleIgnore = async () => {
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
try {
|
try {
|
||||||
await emailsApi.updateStatus(email.Id, 3);
|
await emailsApi.ignore(email.Id);
|
||||||
message.success('E-Mail als ignoriert markiert');
|
message.success('E-Mail als ignoriert markiert');
|
||||||
navigate('/mailpostfach');
|
navigate('/mailpostfach');
|
||||||
} catch (err) {
|
} catch {
|
||||||
message.error('Fehler beim Markieren der E-Mail');
|
message.error('Fehler beim Markieren der E-Mail');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user