fix(auth): Schiffsdaten und Skipper-Profil nur für Logbuch-Eigner
Eingeladene Crew (WRITE) sieht Schiffsdaten und Skipper-Profil schreibgeschützt; Server-Sync lehnt entsprechende Änderungen ab. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+8
-2
@@ -435,6 +435,8 @@ function App() {
|
|||||||
|
|
||||||
const logbookReadOnly =
|
const logbookReadOnly =
|
||||||
activeLogbookRecord?.isShared === 1 && activeAccessRole === 'READ'
|
activeLogbookRecord?.isShared === 1 && activeAccessRole === 'READ'
|
||||||
|
const isLogbookOwner =
|
||||||
|
activeAccessRole === 'OWNER' || activeLogbookRecord?.isShared !== 1
|
||||||
|
|
||||||
if (!activeLogbookId) {
|
if (!activeLogbookId) {
|
||||||
return (
|
return (
|
||||||
@@ -581,11 +583,15 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'vessel' && (
|
{activeTab === 'vessel' && (
|
||||||
<VesselForm logbookId={activeLogbookId} readOnly={logbookReadOnly} />
|
<VesselForm logbookId={activeLogbookId} readOnly={logbookReadOnly || !isLogbookOwner} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'crew' && (
|
{activeTab === 'crew' && (
|
||||||
<CrewForm logbookId={activeLogbookId} readOnly={logbookReadOnly} />
|
<CrewForm
|
||||||
|
logbookId={activeLogbookId}
|
||||||
|
readOnly={logbookReadOnly}
|
||||||
|
skipperReadOnly={!isLogbookOwner}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'stats' && activeLogbookId && activeLogbookTitle && (
|
{activeTab === 'stats' && activeLogbookId && activeLogbookTitle && (
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Users, User, Plus, Trash2, Edit2, Save, X, Check, Camera } from 'lucide
|
|||||||
interface CrewFormProps {
|
interface CrewFormProps {
|
||||||
logbookId: string
|
logbookId: string
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
|
skipperReadOnly?: boolean
|
||||||
preloadedData?: any[]
|
preloadedData?: any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,9 +35,15 @@ interface DecryptedCrew {
|
|||||||
data: CrewMemberData
|
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 { t } = useTranslation()
|
||||||
const { showConfirm } = useDialog()
|
const { showConfirm } = useDialog()
|
||||||
|
const skipperFormReadOnly = readOnly || skipperReadOnly
|
||||||
|
|
||||||
// Skipper profile state
|
// Skipper profile state
|
||||||
const [skipName, setSkipName] = useState('')
|
const [skipName, setSkipName] = useState('')
|
||||||
@@ -192,7 +199,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
|
|
||||||
const handleSaveSkipper = async (e: React.FormEvent) => {
|
const handleSaveSkipper = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (readOnly) return
|
if (skipperFormReadOnly) return
|
||||||
setSavingSkipper(true)
|
setSavingSkipper(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
setSkipperSuccess(false)
|
setSkipperSuccess(false)
|
||||||
@@ -397,10 +404,14 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
|
|
||||||
{error && <div className="auth-error mb-4">{error}</div>}
|
{error && <div className="auth-error mb-4">{error}</div>}
|
||||||
|
|
||||||
|
{skipperReadOnly && !readOnly && (
|
||||||
|
<p className="help-text mb-4">{t('crew.skipper_read_only_hint')}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSaveSkipper} className="vessel-form">
|
<form onSubmit={handleSaveSkipper} className="vessel-form">
|
||||||
<div className="form-grid">
|
<div className="form-grid">
|
||||||
<div className="vessel-photo-wrapper">
|
<div className="vessel-photo-wrapper">
|
||||||
<div className="vessel-photo-preview" onClick={readOnly ? undefined : () => skipFileInputRef.current?.click()} style={{ cursor: readOnly ? 'default' : 'pointer' }}>
|
<div className="vessel-photo-preview" onClick={skipperFormReadOnly ? undefined : () => skipFileInputRef.current?.click()} style={{ cursor: skipperFormReadOnly ? 'default' : 'pointer' }}>
|
||||||
{skipPhoto ? (
|
{skipPhoto ? (
|
||||||
<img src={skipPhoto} alt={skipName || 'Skipper'} className="vessel-photo" />
|
<img src={skipPhoto} alt={skipName || 'Skipper'} className="vessel-photo" />
|
||||||
) : (
|
) : (
|
||||||
@@ -408,7 +419,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
<User size={48} className="placeholder-icon" />
|
<User size={48} className="placeholder-icon" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!readOnly && (
|
{!skipperFormReadOnly && (
|
||||||
<div className="vessel-photo-overlay">
|
<div className="vessel-photo-overlay">
|
||||||
<Camera size={24} />
|
<Camera size={24} />
|
||||||
<span>{skipPhoto ? t('vessel.photo_change') : t('vessel.photo_add')}</span>
|
<span>{skipPhoto ? t('vessel.photo_change') : t('vessel.photo_add')}</span>
|
||||||
@@ -416,7 +427,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!readOnly && (
|
{!skipperFormReadOnly && (
|
||||||
<div className="vessel-photo-actions">
|
<div className="vessel-photo-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -473,7 +484,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
className="input-text"
|
className="input-text"
|
||||||
value={skipName}
|
value={skipName}
|
||||||
onChange={(e) => setSkipName(e.target.value)}
|
onChange={(e) => setSkipName(e.target.value)}
|
||||||
disabled={savingSkipper || readOnly}
|
disabled={savingSkipper || skipperFormReadOnly}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -485,7 +496,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
className="input-text"
|
className="input-text"
|
||||||
value={skipAddress}
|
value={skipAddress}
|
||||||
onChange={(e) => setSkipAddress(e.target.value)}
|
onChange={(e) => setSkipAddress(e.target.value)}
|
||||||
disabled={savingSkipper || readOnly}
|
disabled={savingSkipper || skipperFormReadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -496,7 +507,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
className="input-text"
|
className="input-text"
|
||||||
value={skipBirthDate}
|
value={skipBirthDate}
|
||||||
onChange={(e) => setSkipBirthDate(e.target.value)}
|
onChange={(e) => setSkipBirthDate(e.target.value)}
|
||||||
disabled={savingSkipper || readOnly}
|
disabled={savingSkipper || skipperFormReadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -507,7 +518,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
className="input-text"
|
className="input-text"
|
||||||
value={skipPhone}
|
value={skipPhone}
|
||||||
onChange={(e) => setSkipPhone(e.target.value)}
|
onChange={(e) => setSkipPhone(e.target.value)}
|
||||||
disabled={savingSkipper || readOnly}
|
disabled={savingSkipper || skipperFormReadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -518,7 +529,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
className="input-text"
|
className="input-text"
|
||||||
value={skipNationality}
|
value={skipNationality}
|
||||||
onChange={(e) => setSkipNationality(e.target.value)}
|
onChange={(e) => setSkipNationality(e.target.value)}
|
||||||
disabled={savingSkipper || readOnly}
|
disabled={savingSkipper || skipperFormReadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -529,7 +540,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
className="input-text"
|
className="input-text"
|
||||||
value={skipPassport}
|
value={skipPassport}
|
||||||
onChange={(e) => setSkipPassport(e.target.value)}
|
onChange={(e) => setSkipPassport(e.target.value)}
|
||||||
disabled={savingSkipper || readOnly}
|
disabled={savingSkipper || skipperFormReadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -540,7 +551,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
className="input-text"
|
className="input-text"
|
||||||
value={skipBloodType}
|
value={skipBloodType}
|
||||||
onChange={(e) => setSkipBloodType(e.target.value)}
|
onChange={(e) => setSkipBloodType(e.target.value)}
|
||||||
disabled={savingSkipper || readOnly}
|
disabled={savingSkipper || skipperFormReadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -551,7 +562,7 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
className="input-text"
|
className="input-text"
|
||||||
value={skipAllergies}
|
value={skipAllergies}
|
||||||
onChange={(e) => setSkipAllergies(e.target.value)}
|
onChange={(e) => setSkipAllergies(e.target.value)}
|
||||||
disabled={savingSkipper || readOnly}
|
disabled={savingSkipper || skipperFormReadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -562,12 +573,12 @@ export default function CrewForm({ logbookId, readOnly = false, preloadedData }:
|
|||||||
className="input-text"
|
className="input-text"
|
||||||
value={skipDiseases}
|
value={skipDiseases}
|
||||||
onChange={(e) => setSkipDiseases(e.target.value)}
|
onChange={(e) => setSkipDiseases(e.target.value)}
|
||||||
disabled={savingSkipper || readOnly}
|
disabled={savingSkipper || skipperFormReadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!readOnly && (
|
{!skipperFormReadOnly && (
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
{skipperSuccess && (
|
{skipperSuccess && (
|
||||||
<div className="success-toast">
|
<div className="success-toast">
|
||||||
|
|||||||
@@ -271,6 +271,7 @@
|
|||||||
"crew": {
|
"crew": {
|
||||||
"title": "Skipper- & Crew-Profile",
|
"title": "Skipper- & Crew-Profile",
|
||||||
"skipper_section": "Skipper-Profil",
|
"skipper_section": "Skipper-Profil",
|
||||||
|
"skipper_read_only_hint": "Das Skipper-Profil kann nur vom Logbuch-Eigner bearbeitet werden.",
|
||||||
"crew_section": "Crew-Liste",
|
"crew_section": "Crew-Liste",
|
||||||
"add_crew": "Crew-Mitglied hinzufügen",
|
"add_crew": "Crew-Mitglied hinzufügen",
|
||||||
"edit_crew": "Crew-Mitglied bearbeiten",
|
"edit_crew": "Crew-Mitglied bearbeiten",
|
||||||
|
|||||||
@@ -271,6 +271,7 @@
|
|||||||
"crew": {
|
"crew": {
|
||||||
"title": "Skipper & Crew Profiles",
|
"title": "Skipper & Crew Profiles",
|
||||||
"skipper_section": "Skipper Profile",
|
"skipper_section": "Skipper Profile",
|
||||||
|
"skipper_read_only_hint": "The skipper profile can only be edited by the logbook owner.",
|
||||||
"crew_section": "Crew List",
|
"crew_section": "Crew List",
|
||||||
"add_crew": "Add Crew Member",
|
"add_crew": "Add Crew Member",
|
||||||
"edit_crew": "Edit Crew Member",
|
"edit_crew": "Edit Crew Member",
|
||||||
|
|||||||
@@ -121,6 +121,17 @@ router.post('/push', async (req: any, res) => {
|
|||||||
continue
|
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 (action === 'delete') {
|
||||||
if (type === 'yacht') {
|
if (type === 'yacht') {
|
||||||
await prisma.yachtPayload.deleteMany({ where: { logbookId } })
|
await prisma.yachtPayload.deleteMany({ where: { logbookId } })
|
||||||
|
|||||||
Reference in New Issue
Block a user