fix(nextcloud-files): CR-02 Nextcloud-Adresse aendern nur noch Administratoren
- PUT settings traegt @Roles(ADMIN, SUPER_ADMIN) statt @ModuleManage (Muster wie TendersController.getSourceConfig); alle Benutzer schicken ihr Passwort an diese Adresse - POST settings/test: Verwalter ohne Administratorrolle pruefen nur die gespeicherte Adresse, jede andere Adresse ist 403 - Einstellungen fuer Verwalter nur lesbar mit kurzem Hinweis, ohne Speichern-Knopf; Hinweis "nicht eingerichtet" verweist Nicht-Administratoren an einen Administrator - Metadaten-Spec (module-manage-handlers), Controller-Spec, Seitentest, e2e-settings (Verwalter: 403 auf PUT und fremde Pruefung), Anleitungen und Changelog angepasst Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -136,7 +136,6 @@ describe('Umgestellte Handler (Verwalten)', () => {
|
||||
|
||||
it.each([
|
||||
'getSettings',
|
||||
'saveSettings',
|
||||
'testSettings',
|
||||
])('NextcloudFilesController.%s verlangt Verwalten für nextcloud-files (quick-261008-mzu)', (name) => {
|
||||
expectManage(NextcloudFilesController, name, 'nextcloud-files');
|
||||
@@ -201,4 +200,8 @@ describe('Bewusst nur für Administratoren (T-icv-01, T-icv-07)', () => {
|
||||
])('ModuleRegistryController.%s bleibt @Roles(ADMIN, SUPER_ADMIN)', (name) => {
|
||||
expectAdminOnly(ModuleRegistryController, name);
|
||||
});
|
||||
|
||||
it('NextcloudFilesController.saveSettings: die Nextcloud-Adresse legt nur ein Administrator fest (quick-261008-mzu, CR-02)', () => {
|
||||
expectAdminOnly(NextcloudFilesController, 'saveSettings');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ const proto = NextcloudFilesController.prototype as any;
|
||||
const req = (tenantId?: string) => ({ tenantId }) as any;
|
||||
|
||||
/** Verwalten (Administratoren und Freigabestufe Verwalten). Spaetere Aufgaben ergaenzen nichts hier. */
|
||||
const MANAGE_HANDLERS = ['getSettings', 'saveSettings', 'testSettings'];
|
||||
const MANAGE_HANDLERS = ['getSettings', 'testSettings'];
|
||||
/** Nur Administratoren (CR-02): die Adresse aendern. */
|
||||
const ADMIN_HANDLERS = ['saveSettings'];
|
||||
|
||||
/** Hoch- und Herunterladen (quick-261008-mzu, Aufgabe 4): alle auf Benutzen-Ebene. */
|
||||
const TRANSFER_STATIC = ['download', 'downloadZip', 'startUpload', 'putSingle'];
|
||||
@@ -52,15 +54,24 @@ describe('NextcloudFilesController — Metadaten', () => {
|
||||
expect(Reflect.getMetadata(MODULE_SLUG_KEY, NextcloudFilesController)).toBe('nextcloud-files');
|
||||
});
|
||||
|
||||
it('Einstellungen und Pruefung verlangen Verwalten, ohne Rollen-Decorator', () => {
|
||||
it('Einstellungen lesen und Pruefung verlangen Verwalten, ohne Rollen-Decorator', () => {
|
||||
for (const name of MANAGE_HANDLERS) {
|
||||
expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name]), name).toBe(true);
|
||||
expect(Reflect.getMetadata(ROLES_KEY, proto[name]), name).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('die Adresse aendern duerfen nur Administratoren (CR-02)', () => {
|
||||
for (const name of ADMIN_HANDLERS) {
|
||||
expect(Reflect.getMetadata(ROLES_KEY, proto[name]), name).toEqual(['ADMIN', 'SUPER_ADMIN']);
|
||||
expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name]), name).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('alle anderen Handler stehen auf Benutzen-Ebene', () => {
|
||||
const others = routeHandlers().filter((n) => !MANAGE_HANDLERS.includes(n));
|
||||
const others = routeHandlers().filter(
|
||||
(n) => !MANAGE_HANDLERS.includes(n) && !ADMIN_HANDLERS.includes(n),
|
||||
);
|
||||
expect(others).toContain('getStatus');
|
||||
for (const name of others) {
|
||||
expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name]), name).toBeUndefined();
|
||||
@@ -195,6 +206,9 @@ describe('NextcloudFilesController — Delegation', () => {
|
||||
getSettings: vi.fn(async (..._a: unknown[]) => ({ baseUrl: null, connectedAccounts: 0 })),
|
||||
saveSettings: vi.fn(async (..._a: unknown[]) => ({})),
|
||||
testAddress: vi.fn(async (..._a: unknown[]) => ({ ok: true })),
|
||||
getBaseUrl: vi.fn(
|
||||
async (..._a: unknown[]): Promise<string | null> => 'https://cloud.example',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -224,7 +238,10 @@ describe('NextcloudFilesController — Delegation', () => {
|
||||
|
||||
function makeServerInfo() {
|
||||
return {
|
||||
getServerInfo: vi.fn(async (..._a: unknown[]) => ({ host: 'cloud.example', name: 'Nextcloud' })),
|
||||
getServerInfo: vi.fn(async (..._a: unknown[]) => ({
|
||||
host: 'cloud.example',
|
||||
name: 'Nextcloud',
|
||||
})),
|
||||
sendLogo: vi.fn(async (..._a: unknown[]) => undefined),
|
||||
};
|
||||
}
|
||||
@@ -368,9 +385,10 @@ describe('NextcloudFilesController — Delegation', () => {
|
||||
makeServerInfo() as any,
|
||||
);
|
||||
await controller.getStatus(userReq('t1', 'u1'));
|
||||
const admin = { tenantId: 't1', user: { id: 'a1', role: 'ADMIN' } } as any;
|
||||
await controller.getSettings(req('t1'));
|
||||
await controller.saveSettings(req('t1'), { baseUrl: 'https://x.example' } as any);
|
||||
await controller.testSettings(req('t1'), { baseUrl: 'https://x.example' } as any);
|
||||
await controller.saveSettings(admin, { baseUrl: 'https://x.example' } as any);
|
||||
await controller.testSettings(admin, { baseUrl: 'https://x.example' } as any);
|
||||
await controller.connectPassword(userReq('t1', 'u1'), {
|
||||
loginName: ' anna ',
|
||||
password: 'geheim',
|
||||
@@ -391,6 +409,31 @@ describe('NextcloudFilesController — Delegation', () => {
|
||||
expect(account.disconnect).toHaveBeenCalledWith('t1', 'u1');
|
||||
});
|
||||
|
||||
it('Verwalter ohne Administratorrolle pruefen nur die gespeicherte Adresse (CR-02)', async () => {
|
||||
const settings = makeSettings();
|
||||
const controller = new NextcloudFilesController(
|
||||
settings as any,
|
||||
makeAccount() as any,
|
||||
makeFiles() as any,
|
||||
makeTransfer() as any,
|
||||
makeServerInfo() as any,
|
||||
);
|
||||
const manager = { tenantId: 't1', user: { id: 'm1', role: 'USER' } } as any;
|
||||
await controller.testSettings(manager, { baseUrl: 'https://CLOUD.example/' } as any);
|
||||
expect(settings.testAddress).toHaveBeenCalledTimes(1);
|
||||
await expect(
|
||||
controller.testSettings(manager, { baseUrl: 'http://10.0.0.5:8080' } as any),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
settings.getBaseUrl.mockResolvedValueOnce(null);
|
||||
await expect(
|
||||
controller.testSettings(manager, { baseUrl: 'https://cloud.example' } as any),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
expect(settings.testAddress).toHaveBeenCalledTimes(1);
|
||||
const superAdmin = { tenantId: 't1', user: { id: 's1', role: 'SUPER_ADMIN' } } as any;
|
||||
await controller.testSettings(superAdmin, { baseUrl: 'http://10.0.0.5:8080' } as any);
|
||||
expect(settings.testAddress).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('ohne Mandantenkontext: ForbiddenException', async () => {
|
||||
const controller = new NextcloudFilesController(
|
||||
makeSettings() as any,
|
||||
|
||||
@@ -13,9 +13,12 @@ import {
|
||||
Req,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { Role } from '@prisma/client';
|
||||
import type { Response } from 'express';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import type { AuthenticatedRequest } from '../auth/types/auth-user';
|
||||
import { ModuleManage, UseModule } from '../module-registry/module.guard';
|
||||
import { normalizeCloudUrl } from '../nextcloud-status/nextcloud-status-fetch';
|
||||
import { ConnectPasswordDto } from './dto/nextcloud-files-connect.dto';
|
||||
import {
|
||||
CreateFolderDto,
|
||||
@@ -49,9 +52,14 @@ import { NextcloudServerInfoService } from './nextcloud-server-info';
|
||||
* die Benutzerkennung (spaetere Aufgaben) ausschliesslich aus dem Token.
|
||||
*
|
||||
* Rechte je Route (quick-261008-mzu, D-N):
|
||||
* Nur Administratoren (`@Roles(ADMIN, SUPER_ADMIN)`, CR-02): PUT settings. Die
|
||||
* Adresse bestimmt, wohin ALLE Benutzer ihr Nextcloud-Passwort
|
||||
* schicken (oft das Verzeichnis-Passwort); das darf keine nur
|
||||
* delegierte Freigabe.
|
||||
* Verwalten (`@ModuleManage('nextcloud-files')`, Administratoren und Benutzer
|
||||
* mit der Freigabestufe Verwalten): GET settings, PUT settings,
|
||||
* POST settings/test.
|
||||
* mit der Freigabestufe Verwalten): GET settings, POST settings/test —
|
||||
* wer nicht Administrator ist, darf dabei nur die GESPEICHERTE
|
||||
* Adresse pruefen (keine neue, kein Abtasten anderer Adressen).
|
||||
* Benutzen (nur Klassen-`@UseModule`): alles andere — GET status, GET server,
|
||||
* GET server/logo, POST connect/password, POST connect/flow,
|
||||
* DELETE connect, GET files, DELETE files, POST folders, POST move,
|
||||
@@ -61,7 +69,8 @@ import { NextcloudServerInfoService } from './nextcloud-server-info';
|
||||
* uploads/:uploadId/complete, GET uploads/:uploadId/state, DELETE
|
||||
* uploads/:uploadId. Jeder Benutzer arbeitet nur im eigenen Konto.
|
||||
* Auf Verwalten-Handlern steht NIE ein Rollen-Decorator — der globale
|
||||
* RolesGuard wuerde Verwalter sonst aussperren.
|
||||
* RolesGuard wuerde Verwalter sonst aussperren. Der reine Administrator-Handler
|
||||
* traegt dafuer kein `@ModuleManage` (Muster wie `TendersController.getSourceConfig`).
|
||||
*
|
||||
* REIHENFOLGE: alle statischen Routen stehen VOR jeder Route mit `:param`, sonst
|
||||
* faengt die Parameterroute sie ab (404-Shadowing, Unit-Tests ohne die
|
||||
@@ -78,6 +87,14 @@ function headerOf(value: string | string[] | undefined): string | undefined {
|
||||
return Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
|
||||
const ADDRESS_ADMIN_ONLY =
|
||||
'Nur Administratoren dürfen die Nextcloud-Adresse ändern oder eine andere Adresse prüfen.';
|
||||
|
||||
function isAdmin(req: AuthenticatedRequest): boolean {
|
||||
const role = req.user?.role;
|
||||
return role === Role.ADMIN || role === Role.SUPER_ADMIN;
|
||||
}
|
||||
|
||||
@Controller('modules/nextcloud-files')
|
||||
@UseModule('nextcloud-files')
|
||||
export class NextcloudFilesController {
|
||||
@@ -129,8 +146,9 @@ export class NextcloudFilesController {
|
||||
return this.settings.getSettings(this.requireTenantId(req));
|
||||
}
|
||||
|
||||
/** Die Adresse legt nur ein Administrator fest (CR-02). */
|
||||
@Put('settings')
|
||||
@ModuleManage('nextcloud-files')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async saveSettings(@Req() req: AuthenticatedRequest, @Body() dto: SaveNextcloudFilesSettingsDto) {
|
||||
return this.settings.saveSettings(this.requireTenantId(req), dto);
|
||||
}
|
||||
@@ -139,7 +157,14 @@ export class NextcloudFilesController {
|
||||
@HttpCode(200)
|
||||
@ModuleManage('nextcloud-files')
|
||||
async testSettings(@Req() req: AuthenticatedRequest, @Body() dto: TestNextcloudFilesSettingsDto) {
|
||||
this.requireTenantId(req);
|
||||
const tenantId = this.requireTenantId(req);
|
||||
if (!isAdmin(req)) {
|
||||
// Verwalter ohne Administratorrolle pruefen nur die gespeicherte Adresse (CR-02, WR-01).
|
||||
const saved = await this.settings.getBaseUrl(tenantId);
|
||||
if (saved === null || normalizeCloudUrl(dto.baseUrl) !== saved) {
|
||||
throw new ForbiddenException(ADDRESS_ADMIN_ONLY);
|
||||
}
|
||||
}
|
||||
return this.settings.testAddress(dto.baseUrl);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
saveNextcloudFilesSettings,
|
||||
testNextcloudFilesSettings,
|
||||
} from '@/lib/nextcloud-files-api';
|
||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||
|
||||
const INPUT_CLASS =
|
||||
'w-full rounded border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring';
|
||||
@@ -41,11 +42,16 @@ function MessageLine({ message }: { message: Message | null }) {
|
||||
* Einstellungen des Moduls Dateien (quick-261008-mzu): die EINE Nextcloud-Adresse
|
||||
* der Organisation, die Verbindungspruefung und Hinweise fuer die
|
||||
* Nextcloud-Administration. Nur fuer Verwalter sichtbar (die Seite blendet den
|
||||
* Reiter sonst aus; bindend ist die API).
|
||||
* Reiter sonst aus; bindend ist die API). Die Adresse AENDERN duerfen nur
|
||||
* Administratoren (CR-02): alle Benutzer schicken ihr Nextcloud-Passwort an diese
|
||||
* Adresse. Verwalter ohne Administratorrolle sehen sie nur und koennen die
|
||||
* gespeicherte Adresse pruefen.
|
||||
*/
|
||||
export function SettingsTab({ onChanged }: { onChanged: () => void }) {
|
||||
const t = useTranslations('nextcloudFiles.settings');
|
||||
const tErrors = useTranslations('nextcloudFiles.errors');
|
||||
const role = useAuthStore((s) => s.user?.role);
|
||||
const isAdmin = role === 'ADMIN' || role === 'SUPER_ADMIN';
|
||||
|
||||
const [settings, setSettings] = useState<NextcloudFilesSettings | null>(null);
|
||||
const [address, setAddress] = useState('');
|
||||
@@ -81,7 +87,9 @@ export function SettingsTab({ onChanged }: { onChanged: () => void }) {
|
||||
setTesting(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
showCheck(await testNextcloudFilesSettings(address.trim()));
|
||||
// Ohne Administratorrolle pruefen nur die gespeicherte Adresse (die API lehnt andere ab).
|
||||
const target = isAdmin ? address.trim() : (settings?.baseUrl ?? '');
|
||||
showCheck(await testNextcloudFilesSettings(target));
|
||||
} catch (error) {
|
||||
setMessage({ kind: 'error', text: errorText(error) });
|
||||
} finally {
|
||||
@@ -128,7 +136,7 @@ export function SettingsTab({ onChanged }: { onChanged: () => void }) {
|
||||
};
|
||||
|
||||
const busy = testing || saving;
|
||||
const empty = address.trim() === '';
|
||||
const empty = isAdmin ? address.trim() === '' : !settings?.baseUrl;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -148,14 +156,16 @@ export function SettingsTab({ onChanged }: { onChanged: () => void }) {
|
||||
>
|
||||
{testing ? t('address.testing') : t('address.test')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={PRIMARY_BUTTON}
|
||||
disabled={busy || empty || loadFailed}
|
||||
onClick={onSaveClick}
|
||||
>
|
||||
{saving ? t('saving') : t('save')}
|
||||
</button>
|
||||
{isAdmin && (
|
||||
<button
|
||||
type="button"
|
||||
className={PRIMARY_BUTTON}
|
||||
disabled={busy || empty || loadFailed}
|
||||
onClick={onSaveClick}
|
||||
>
|
||||
{saving ? t('saving') : t('save')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -200,15 +210,22 @@ export function SettingsTab({ onChanged }: { onChanged: () => void }) {
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
className={INPUT_CLASS}
|
||||
className={`${INPUT_CLASS} read-only:cursor-default read-only:bg-well read-only:text-muted-foreground`}
|
||||
value={address}
|
||||
placeholder={t('address.placeholder')}
|
||||
readOnly={!isAdmin}
|
||||
aria-describedby={isAdmin ? undefined : 'nextcloud-files-address-admin-only'}
|
||||
placeholder={isAdmin ? t('address.placeholder') : undefined}
|
||||
onChange={(event) => {
|
||||
setAddress(event.target.value);
|
||||
setMessage(null);
|
||||
setConfirmCount(null);
|
||||
}}
|
||||
/>
|
||||
{!isAdmin && (
|
||||
<p id="nextcloud-files-address-admin-only" className="text-xs text-muted-foreground">
|
||||
{t('address.adminOnly')}
|
||||
</p>
|
||||
)}
|
||||
{settings?.baseUrl && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('address.connectedUsers', { count: settings.connectedAccounts })}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type NextcloudFilesStatus,
|
||||
type NextcloudServerInfo,
|
||||
} from '@/lib/nextcloud-files-api';
|
||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||
import de from '@/messages/de.json';
|
||||
import NextcloudFilesPage from './page';
|
||||
|
||||
@@ -90,6 +91,7 @@ function settings(over: Partial<NextcloudFilesSettings> = {}): NextcloudFilesSet
|
||||
|
||||
beforeEach(() => {
|
||||
mockCanManage = true;
|
||||
useAuthStore.setState({ user: { id: 'a1', role: 'ADMIN' } as never });
|
||||
mockGetStatus.mockReset().mockResolvedValue(status());
|
||||
mockGetSettings.mockReset().mockResolvedValue(settings());
|
||||
mockSaveSettings.mockReset();
|
||||
@@ -135,6 +137,7 @@ describe('NextcloudFilesPage', () => {
|
||||
|
||||
it('nicht eingerichtet, Benutzen: Hinweis auf einen Administrator, kein Reiter, kein Einstellungsknopf', async () => {
|
||||
mockCanManage = false;
|
||||
useAuthStore.setState({ user: { id: 'u1', role: 'USER' } as never });
|
||||
mockGetStatus.mockResolvedValue(status({ configured: false, serverUrl: null, host: null }));
|
||||
render(<NextcloudFilesPage />);
|
||||
expect(await screen.findByText(/Bitte wenden Sie sich an einen Administrator/)).toBeTruthy();
|
||||
@@ -144,6 +147,14 @@ describe('NextcloudFilesPage', () => {
|
||||
expect(mockGetSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('nicht eingerichtet, Verwalten ohne Administratorrolle: Hinweis auf einen Administrator, kein Knopf (CR-02)', async () => {
|
||||
useAuthStore.setState({ user: { id: 'm1', role: 'USER' } as never });
|
||||
mockGetStatus.mockResolvedValue(status({ configured: false, serverUrl: null, host: null }));
|
||||
render(<NextcloudFilesPage />);
|
||||
expect(await screen.findByText(/Nur er kann die Adresse der Nextcloud eintragen/)).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: 'Zu den Einstellungen' })).toBeNull();
|
||||
});
|
||||
|
||||
it('Verwalter sehen die Reiter Dateien und Einstellungen', async () => {
|
||||
render(<NextcloudFilesPage />);
|
||||
expect(await screen.findByRole('button', { name: 'Dateien' })).toBeTruthy();
|
||||
@@ -256,6 +267,26 @@ describe('SettingsTab', () => {
|
||||
expect(await screen.findByText('Verbindung erfolgreich.')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Verwalten ohne Administratorrolle: Adresse nur lesbar, kein Speichern, Pruefung der gespeicherten Adresse (CR-02)', async () => {
|
||||
useAuthStore.setState({ user: { id: 'm1', role: 'USER' } as never });
|
||||
mockTestSettings.mockResolvedValue({
|
||||
ok: true,
|
||||
kind: 'ok',
|
||||
message: 'Verbindung erfolgreich.',
|
||||
version: null,
|
||||
productName: null,
|
||||
});
|
||||
const input = (await openSettings()) as HTMLInputElement;
|
||||
await waitFor(() => expect(input.value).toBe('https://cloud.example'));
|
||||
expect(input.readOnly).toBe(true);
|
||||
expect(screen.getByText(/Die Adresse legt ein Administrator fest/)).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: 'Speichern' })).toBeNull();
|
||||
fireEvent.change(input, { target: { value: 'http://10.0.0.5' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Verbindung prüfen' }));
|
||||
await waitFor(() => expect(mockTestSettings).toHaveBeenCalledWith('https://cloud.example'));
|
||||
expect(mockSaveSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('die Hinweiskarte nennt den Befehl fuer die Ausnahmeliste', async () => {
|
||||
await openSettings();
|
||||
expect(screen.getByText(/occ config:app:set bruteForce whitelist_0/)).toBeTruthy();
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type NextcloudFilesStatus,
|
||||
type NextcloudServerInfo,
|
||||
} from '@/lib/nextcloud-files-api';
|
||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||
import { useCanManageModule } from '@/lib/use-module-capability';
|
||||
import { AccountBar } from './components/AccountBar';
|
||||
import { ConnectPanel } from './components/ConnectPanel';
|
||||
@@ -21,9 +22,10 @@ type TabId = 'files' | 'settings';
|
||||
|
||||
/**
|
||||
* Dateien (quick-261008-mzu): die Nextcloud-Dateien jedes Benutzers in Tessera.
|
||||
* Die Adresse der Nextcloud stellen Administratoren und Benutzer mit der
|
||||
* Freigabestufe Verwalten im Reiter "Einstellungen" ein; alle anderen sehen nur
|
||||
* den Reiter "Dateien" ohne Reiterleiste — bindend ist allein die API. Im
|
||||
* Den Reiter "Einstellungen" sehen Administratoren und Benutzer mit der
|
||||
* Freigabestufe Verwalten; die Adresse der Nextcloud AENDERN darf nur ein
|
||||
* Administrator (CR-02). Alle anderen sehen nur den Reiter "Dateien" ohne
|
||||
* Reiterleiste — bindend ist allein die API. Im
|
||||
* Abschnitt `nextcloud-files-main` verbindet jeder Benutzer sein eigenes Konto
|
||||
* (ConnectPanel); ist es verbunden, steht dort die Dateiansicht
|
||||
* (`nextcloud-files-browser`, FileBrowser). Laeuft die Verbindung ab, laedt die
|
||||
@@ -32,6 +34,8 @@ type TabId = 'files' | 'settings';
|
||||
export default function NextcloudFilesPage() {
|
||||
const t = useTranslations('nextcloudFiles');
|
||||
const canManage = useCanManageModule('nextcloud-files') === true;
|
||||
const role = useAuthStore((s) => s.user?.role);
|
||||
const isAdmin = role === 'ADMIN' || role === 'SUPER_ADMIN';
|
||||
|
||||
const [status, setStatus] = useState<NextcloudFilesStatus | null>(null);
|
||||
const [statusError, setStatusError] = useState(false);
|
||||
@@ -107,9 +111,9 @@ export default function NextcloudFilesPage() {
|
||||
{activeTab === 'files' && status && !status.configured && (
|
||||
<SettingsSection title={t('notConfigured.title')}>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{canManage ? t('notConfigured.managerHint') : t('notConfigured.userHint')}
|
||||
{isAdmin ? t('notConfigured.managerHint') : t('notConfigured.userHint')}
|
||||
</p>
|
||||
{canManage && (
|
||||
{isAdmin && canManage && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTab('settings')}
|
||||
|
||||
@@ -2331,7 +2331,7 @@
|
||||
"notConfigured": {
|
||||
"title": "Die Nextcloud ist noch nicht eingerichtet.",
|
||||
"managerHint": "Tragen Sie in den Einstellungen die Adresse Ihrer Nextcloud ein.",
|
||||
"userHint": "Bitte wenden Sie sich an einen Administrator oder an jemanden mit der Freigabestufe Verwalten.",
|
||||
"userHint": "Bitte wenden Sie sich an einen Administrator. Nur er kann die Adresse der Nextcloud eintragen.",
|
||||
"goToSettings": "Zu den Einstellungen"
|
||||
},
|
||||
"connect": {
|
||||
@@ -2390,7 +2390,8 @@
|
||||
"placeholder": "https://cloud.ihre-firma.de",
|
||||
"test": "Verbindung prüfen",
|
||||
"testing": "Prüfe …",
|
||||
"connectedUsers": "{count, plural, =0 {Zurzeit ist niemand verbunden.} one {Ein Benutzer ist verbunden.} other {# Benutzer sind verbunden.}}"
|
||||
"connectedUsers": "{count, plural, =0 {Zurzeit ist niemand verbunden.} one {Ein Benutzer ist verbunden.} other {# Benutzer sind verbunden.}}",
|
||||
"adminOnly": "Die Adresse legt ein Administrator fest. Sie können sie hier ansehen und die Verbindung prüfen."
|
||||
},
|
||||
"reconnect": {
|
||||
"message": "{count, plural, one {Ein Benutzer ist verbunden. Nach dem Wechsel muss er sich neu anmelden.} other {# Benutzer sind verbunden. Nach dem Wechsel müssen sich alle neu anmelden.}}",
|
||||
|
||||
@@ -2331,7 +2331,7 @@
|
||||
"notConfigured": {
|
||||
"title": "Nextcloud has not been set up yet.",
|
||||
"managerHint": "Enter the address of your Nextcloud in the settings.",
|
||||
"userHint": "Please contact an administrator or someone with the Manage permission level.",
|
||||
"userHint": "Please contact an administrator. Only an administrator can enter the Nextcloud address.",
|
||||
"goToSettings": "Go to settings"
|
||||
},
|
||||
"connect": {
|
||||
@@ -2390,7 +2390,8 @@
|
||||
"placeholder": "https://cloud.your-company.com",
|
||||
"test": "Check connection",
|
||||
"testing": "Checking …",
|
||||
"connectedUsers": "{count, plural, =0 {Nobody is connected right now.} one {One user is connected.} other {# users are connected.}}"
|
||||
"connectedUsers": "{count, plural, =0 {Nobody is connected right now.} one {One user is connected.} other {# users are connected.}}",
|
||||
"adminOnly": "The address is set by an administrator. You can view it here and check the connection."
|
||||
},
|
||||
"reconnect": {
|
||||
"message": "{count, plural, one {One user is connected. After the change they have to sign in again.} other {# users are connected. After the change everyone has to sign in again.}}",
|
||||
|
||||
Reference in New Issue
Block a user