From 5ea5111ec359d723f04bfedfc671399aa9237645 Mon Sep 17 00:00:00 2001 From: elpatron Date: Sat, 30 May 2026 19:17:45 +0200 Subject: [PATCH] =?UTF-8?q?fix(auth):=20Schiffsdaten=20und=20Skipper-Profi?= =?UTF-8?q?l=20nur=20f=C3=BCr=20Logbuch-Eigner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eingeladene Crew (WRITE) sieht Schiffsdaten und Skipper-Profil schreibgeschützt; Server-Sync lehnt entsprechende Änderungen ab. Co-authored-by: Cursor --- client/src/App.tsx | 10 ++++++-- client/src/components/CrewForm.tsx | 41 +++++++++++++++++++----------- client/src/i18n/locales/de.json | 1 + client/src/i18n/locales/en.json | 1 + server/src/routes/sync.ts | 11 ++++++++ 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 098c544..c30b015 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -435,6 +435,8 @@ function App() { const logbookReadOnly = activeLogbookRecord?.isShared === 1 && activeAccessRole === 'READ' + const isLogbookOwner = + activeAccessRole === 'OWNER' || activeLogbookRecord?.isShared !== 1 if (!activeLogbookId) { return ( @@ -581,11 +583,15 @@ function App() { )} {activeTab === 'vessel' && ( - + )} {activeTab === 'crew' && ( - + )} {activeTab === 'stats' && activeLogbookId && activeLogbookTitle && ( diff --git a/client/src/components/CrewForm.tsx b/client/src/components/CrewForm.tsx index d8da019..ebaeaf0 100644 --- a/client/src/components/CrewForm.tsx +++ b/client/src/components/CrewForm.tsx @@ -12,6 +12,7 @@ import { Users, User, Plus, Trash2, Edit2, Save, X, Check, Camera } from 'lucide interface CrewFormProps { logbookId: string readOnly?: boolean + skipperReadOnly?: boolean preloadedData?: any[] } @@ -34,9 +35,15 @@ interface DecryptedCrew { data: CrewMemberData } -export default function CrewForm({ logbookId, readOnly = false, preloadedData }: CrewFormProps) { +export default function CrewForm({ + logbookId, + readOnly = false, + skipperReadOnly = false, + preloadedData +}: CrewFormProps) { const { t } = useTranslation() const { showConfirm } = useDialog() + const skipperFormReadOnly = readOnly || skipperReadOnly // Skipper profile state const [skipName, setSkipName] = useState('') @@ -192,7 +199,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: const handleSaveSkipper = async (e: React.FormEvent) => { e.preventDefault() - if (readOnly) return + if (skipperFormReadOnly) return setSavingSkipper(true) setError(null) setSkipperSuccess(false) @@ -397,10 +404,14 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: {error &&
{error}
} + {skipperReadOnly && !readOnly && ( +

{t('crew.skipper_read_only_hint')}

+ )} +
-
skipFileInputRef.current?.click()} style={{ cursor: readOnly ? 'default' : 'pointer' }}> +
skipFileInputRef.current?.click()} style={{ cursor: skipperFormReadOnly ? 'default' : 'pointer' }}> {skipPhoto ? ( {skipName ) : ( @@ -408,7 +419,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
)} - {!readOnly && ( + {!skipperFormReadOnly && (
{skipPhoto ? t('vessel.photo_change') : t('vessel.photo_add')} @@ -416,7 +427,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: )}
- {!readOnly && ( + {!skipperFormReadOnly && (
@@ -485,7 +496,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: className="input-text" value={skipAddress} onChange={(e) => setSkipAddress(e.target.value)} - disabled={savingSkipper || readOnly} + disabled={savingSkipper || skipperFormReadOnly} />
@@ -496,7 +507,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: className="input-text" value={skipBirthDate} onChange={(e) => setSkipBirthDate(e.target.value)} - disabled={savingSkipper || readOnly} + disabled={savingSkipper || skipperFormReadOnly} />
@@ -507,7 +518,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: className="input-text" value={skipPhone} onChange={(e) => setSkipPhone(e.target.value)} - disabled={savingSkipper || readOnly} + disabled={savingSkipper || skipperFormReadOnly} />
@@ -518,7 +529,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: className="input-text" value={skipNationality} onChange={(e) => setSkipNationality(e.target.value)} - disabled={savingSkipper || readOnly} + disabled={savingSkipper || skipperFormReadOnly} /> @@ -529,7 +540,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: className="input-text" value={skipPassport} onChange={(e) => setSkipPassport(e.target.value)} - disabled={savingSkipper || readOnly} + disabled={savingSkipper || skipperFormReadOnly} /> @@ -540,7 +551,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: className="input-text" value={skipBloodType} onChange={(e) => setSkipBloodType(e.target.value)} - disabled={savingSkipper || readOnly} + disabled={savingSkipper || skipperFormReadOnly} /> @@ -551,7 +562,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: className="input-text" value={skipAllergies} onChange={(e) => setSkipAllergies(e.target.value)} - disabled={savingSkipper || readOnly} + disabled={savingSkipper || skipperFormReadOnly} /> @@ -562,12 +573,12 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }: className="input-text" value={skipDiseases} onChange={(e) => setSkipDiseases(e.target.value)} - disabled={savingSkipper || readOnly} + disabled={savingSkipper || skipperFormReadOnly} /> - {!readOnly && ( + {!skipperFormReadOnly && (
{skipperSuccess && (
diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json index 4f2ff26..267eb1f 100644 --- a/client/src/i18n/locales/de.json +++ b/client/src/i18n/locales/de.json @@ -271,6 +271,7 @@ "crew": { "title": "Skipper- & Crew-Profile", "skipper_section": "Skipper-Profil", + "skipper_read_only_hint": "Das Skipper-Profil kann nur vom Logbuch-Eigner bearbeitet werden.", "crew_section": "Crew-Liste", "add_crew": "Crew-Mitglied hinzufügen", "edit_crew": "Crew-Mitglied bearbeiten", diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index 4714942..b4dedf9 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -271,6 +271,7 @@ "crew": { "title": "Skipper & Crew Profiles", "skipper_section": "Skipper Profile", + "skipper_read_only_hint": "The skipper profile can only be edited by the logbook owner.", "crew_section": "Crew List", "add_crew": "Add Crew Member", "edit_crew": "Edit Crew Member", diff --git a/server/src/routes/sync.ts b/server/src/routes/sync.ts index dc7891f..359e2ae 100644 --- a/server/src/routes/sync.ts +++ b/server/src/routes/sync.ts @@ -121,6 +121,17 @@ router.post('/push', async (req: any, res) => { continue } + if (!isOwner && (type === 'yacht' || (type === 'crew' && payloadId === 'skipper'))) { + results.push({ + payloadId, + status: 'error', + error: type === 'yacht' + ? 'Forbidden: Only owner can modify vessel data' + : 'Forbidden: Only owner can modify skipper profile' + }) + continue + } + if (action === 'delete') { if (type === 'yacht') { await prisma.yachtPayload.deleteMany({ where: { logbookId } })