Исправление дублирования чужих целей в wishlist и защита от редактирования
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m16s

This commit is contained in:
poignatov
2026-01-19 21:56:57 +03:00
parent 6d468d6967
commit ab9022a585
5 changed files with 81 additions and 23 deletions

View File

@@ -1 +1 @@
3.14.6 3.14.7

View File

@@ -9498,6 +9498,14 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
DisplayOrder: int(displayOrder.Int64), DisplayOrder: int(displayOrder.Int64),
} }
// Заполняем UserID для условия
if conditionUserID.Valid {
conditionOwnerID := int(conditionUserID.Int64)
condition.UserID = &conditionOwnerID
} else {
condition.UserID = &userID
}
if taskConditionID.Valid { if taskConditionID.Valid {
condition.Type = "task_completion" condition.Type = "task_completion"
if taskName.Valid { if taskName.Valid {
@@ -9824,12 +9832,21 @@ func (a *App) saveWishlistConditions(
} }
// Определяем user_id для условия: // Определяем user_id для условия:
// - Если условие имеет id и это условие существовало - используем его оригинальный user_id // - Если условие имеет id и это условие существовало - проверяем, принадлежит ли оно текущему пользователю
// - Если условие принадлежит другому пользователю - пропускаем (не сохраняем)
// - Если условие имеет id, но не существовало (например, было только что добавлено) - игнорируем
// - Иначе - используем userID текущего пользователя // - Иначе - используем userID текущего пользователя
conditionUserID := userID conditionUserID := userID
if condition.ID != nil { if condition.ID != nil {
if originalUserID, exists := existingConditions[*condition.ID]; exists { if originalUserID, exists := existingConditions[*condition.ID]; exists {
// Если условие принадлежит другому пользователю - пропускаем (не сохраняем)
if originalUserID != userID {
continue
}
conditionUserID = originalUserID conditionUserID = originalUserID
} else {
// Условие имеет id, но не существует в базе - пропускаем (не сохраняем)
continue
} }
} }
@@ -10258,6 +10275,9 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
conditionOwnerID := itemOwnerID conditionOwnerID := itemOwnerID
if conditionUserID.Valid { if conditionUserID.Valid {
conditionOwnerID = int(conditionUserID.Int64) conditionOwnerID = int(conditionUserID.Int64)
condition.UserID = &conditionOwnerID
} else {
condition.UserID = &itemOwnerID
} }
if taskConditionID.Valid { if taskConditionID.Valid {
@@ -10575,6 +10595,9 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
conditionOwnerID := itemOwnerID conditionOwnerID := itemOwnerID
if conditionUserID.Valid { if conditionUserID.Valid {
conditionOwnerID = int(conditionUserID.Int64) conditionOwnerID = int(conditionUserID.Int64)
condition.UserID = &conditionOwnerID
} else {
condition.UserID = &itemOwnerID
} }
if taskConditionID.Valid { if taskConditionID.Valid {
@@ -12387,6 +12410,10 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
conditionOwnerID := int(itemOwnerID.Int64) conditionOwnerID := int(itemOwnerID.Int64)
if conditionUserID.Valid { if conditionUserID.Valid {
conditionOwnerID = int(conditionUserID.Int64) conditionOwnerID = int(conditionUserID.Int64)
condition.UserID = &conditionOwnerID
} else {
itemOwnerIDVal := int(itemOwnerID.Int64)
condition.UserID = &itemOwnerIDVal
} }
if taskConditionID.Valid { if taskConditionID.Valid {

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "3.14.6", "version": "3.14.7",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -215,6 +215,16 @@
color: #3498db; color: #3498db;
} }
.condition-item-text.condition-item-other-user {
color: #95a5a6;
font-style: italic;
cursor: not-allowed;
}
.condition-item-text.condition-item-other-user:hover {
color: #95a5a6;
}
.remove-condition-button { .remove-condition-button {
background: #e74c3c; background: #e74c3c;
color: white; color: white;

View File

@@ -10,7 +10,7 @@ const PROJECTS_API_URL = '/projects'
const WISHLIST_FORM_STATE_KEY = 'wishlistFormPendingState' const WISHLIST_FORM_STATE_KEY = 'wishlistFormPendingState'
function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, boardId }) { function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, boardId }) {
const { authFetch } = useAuth() const { authFetch, user } = useAuth()
const [name, setName] = useState('') const [name, setName] = useState('')
const [price, setPrice] = useState('') const [price, setPrice] = useState('')
const [link, setLink] = useState('') const [link, setLink] = useState('')
@@ -97,6 +97,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
required_points: cond.required_points || null, required_points: cond.required_points || null,
start_date: cond.start_date || null, start_date: cond.start_date || null,
display_order: idx, display_order: idx,
user_id: cond.user_id || null,
}))) })))
} else { } else {
setUnlockConditions([]) setUnlockConditions([])
@@ -251,6 +252,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
required_points: cond.required_points || null, required_points: cond.required_points || null,
start_date: cond.start_date || null, start_date: cond.start_date || null,
display_order: idx, display_order: idx,
user_id: cond.user_id || null,
}))) })))
} else { } else {
setUnlockConditions([]) setUnlockConditions([])
@@ -469,6 +471,12 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
} }
const handleEditCondition = (index) => { const handleEditCondition = (index) => {
const condition = unlockConditions[index]
// Проверяем, что условие принадлежит текущему пользователю
if (condition.user_id && condition.user_id !== user?.id) {
setToastMessage({ text: 'Нельзя редактировать чужие цели', type: 'error' })
return
}
setEditingConditionIndex(index) setEditingConditionIndex(index)
setShowConditionForm(true) setShowConditionForm(true)
} }
@@ -493,6 +501,12 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
} }
const handleRemoveCondition = (index) => { const handleRemoveCondition = (index) => {
const condition = unlockConditions[index]
// Проверяем, что условие принадлежит текущему пользователю
if (condition.user_id && condition.user_id !== user?.id) {
setToastMessage({ text: 'Нельзя удалять чужие цели', type: 'error' })
return
}
setUnlockConditions(unlockConditions.filter((_, i) => i !== index)) setUnlockConditions(unlockConditions.filter((_, i) => i !== index))
} }
@@ -783,25 +797,32 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
<label>Цель</label> <label>Цель</label>
{unlockConditions.length > 0 && ( {unlockConditions.length > 0 && (
<div className="conditions-list"> <div className="conditions-list">
{unlockConditions.map((cond, idx) => ( {unlockConditions.map((cond, idx) => {
<div key={idx} className="condition-item"> const isOwnCondition = !cond.user_id || cond.user_id === user?.id
<span return (
className="condition-item-text" <div key={idx} className="condition-item">
onClick={() => handleEditCondition(idx)} <span
> className={`condition-item-text ${!isOwnCondition ? 'condition-item-other-user' : ''}`}
{cond.type === 'task_completion' onClick={() => isOwnCondition && handleEditCondition(idx)}
? `Задача: ${tasks.find(t => t.id === cond.task_id)?.name || 'Не выбрана'}` style={{ cursor: isOwnCondition ? 'pointer' : 'default' }}
: `Баллы: ${cond.required_points} в ${projects.find(p => p.project_id === cond.project_id)?.project_name || cond.project_name || 'Не выбран'}${cond.start_date ? ` с ${new Date(cond.start_date + 'T00:00:00').toLocaleDateString('ru-RU')}` : ' за всё время'}`} title={!isOwnCondition ? 'Чужая цель - нельзя редактировать' : ''}
</span> >
<button {cond.type === 'task_completion'
type="button" ? `Задача: ${tasks.find(t => t.id === cond.task_id)?.name || 'Не выбрана'}`
onClick={() => handleRemoveCondition(idx)} : `Баллы: ${cond.required_points} в ${projects.find(p => p.project_id === cond.project_id)?.project_name || cond.project_name || 'Не выбран'}${cond.start_date ? ` с ${new Date(cond.start_date + 'T00:00:00').toLocaleDateString('ru-RU')}` : ' за всё время'}`}
className="remove-condition-button" </span>
> {isOwnCondition && (
<button
</button> type="button"
</div> onClick={() => handleRemoveCondition(idx)}
))} className="remove-condition-button"
>
</button>
)}
</div>
)
})}
</div> </div>
)} )}
<button <button