From 53ee369589ed143210d41a75af2186105c1fc633 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Wed, 26 Nov 2025 17:20:12 +0100 Subject: [PATCH] verification revocation --- app/Http/Controllers/ResultController.php | 18 +++++++ app/Models/Protocol.php | 15 ++++++ app/Policies/ResultPolicy.php | 12 +++++ resources/js/components/protocol/Item.jsx | 2 + .../js/components/results/Verification.jsx | 48 +++++++++++++++---- resources/js/helpers/permissions.js | 3 ++ resources/js/i18n/de.js | 4 ++ resources/js/i18n/en.js | 4 ++ resources/js/pages/Tournament.jsx | 12 +++++ routes/api.php | 1 + 10 files changed, 109 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/ResultController.php b/app/Http/Controllers/ResultController.php index ec732b6..78a1c68 100644 --- a/app/Http/Controllers/ResultController.php +++ b/app/Http/Controllers/ResultController.php @@ -89,6 +89,24 @@ class ResultController extends Controller return $result->toJson(); } + public function unverify(Request $request, Result $result) { + $this->authorize('unverify', $result); + + $result->verified_at = null; + $result->verified_by()->associate(null); + $result->save(); + + Protocol::resultUnverified( + $result->round->tournament, + $result, + $request->user(), + ); + + ResultChanged::dispatch($result); + + return $result->toJson(); + } + public function verify(Request $request, Result $result) { $this->authorize('verify', $result); diff --git a/app/Models/Protocol.php b/app/Models/Protocol.php index ed4ec16..07a1491 100644 --- a/app/Models/Protocol.php +++ b/app/Models/Protocol.php @@ -94,6 +94,21 @@ class Protocol extends Model ProtocolAdded::dispatch($protocol); } + public static function resultUnverified(Tournament $tournament, Result $result, User $user) { + $protocol = static::create([ + 'tournament_id' => $tournament->id, + 'user_id' => $user->id, + 'type' => 'result.unverify', + 'details' => [ + 'tournament' => static::tournamentMemo($tournament), + 'result' => static::resultMemo($result), + 'runner' => static::userMemo($result->user), + 'round' => static::roundMemo($result->round), + ], + ]); + ProtocolAdded::dispatch($protocol); + } + public static function resultVerified(Tournament $tournament, Result $result, User $user) { $protocol = static::create([ 'tournament_id' => $tournament->id, diff --git a/app/Policies/ResultPolicy.php b/app/Policies/ResultPolicy.php index 3e0e4ea..4c4bff5 100644 --- a/app/Policies/ResultPolicy.php +++ b/app/Policies/ResultPolicy.php @@ -92,6 +92,18 @@ class ResultPolicy return false; } + /** + * Determine whether the user can unverify the result. + * + * @param \App\Models\User $user + * @param \App\Models\Result $result + * @return \Illuminate\Auth\Access\Response|bool + */ + public function unverify(User $user, Result $result) + { + return $user->isTournamentCrew($result->round->tournament); + } + /** * Determine whether the user can verify the result. * diff --git a/resources/js/components/protocol/Item.jsx b/resources/js/components/protocol/Item.jsx index b7d6052..e7134f9 100644 --- a/resources/js/components/protocol/Item.jsx +++ b/resources/js/components/protocol/Item.jsx @@ -94,6 +94,7 @@ const getEntryDescription = (entry, t) => { {{time}}, ; } + case 'result.unverify': case 'result.verify': { const number = getEntryRoundNumber(entry); const runner = getEntryResultRunner(entry); @@ -141,6 +142,7 @@ const getEntryIcon = entry => { return ; case 'round.create': return ; + case 'result.unverify': case 'round.delete': return ; case 'round.edit': diff --git a/resources/js/components/results/Verification.jsx b/resources/js/components/results/Verification.jsx index 281c979..43113c5 100644 --- a/resources/js/components/results/Verification.jsx +++ b/resources/js/components/results/Verification.jsx @@ -5,10 +5,11 @@ import { Trans, useTranslation } from 'react-i18next'; import Icon from '../common/Icon'; import Box from '../users/Box'; -import { mayVerifyResult } from '../../helpers/permissions'; +import { mayUnverifyResult, mayVerifyResult } from '../../helpers/permissions'; import { useUser } from '../../hooks/user'; const Verification = ({ actions, result, round, tournament }) => { + const [unverifying, setUnverifying] = React.useState(false); const [verifying, setVerifying] = React.useState(false); const { t } = useTranslation(); @@ -19,6 +20,21 @@ const Verification = ({ actions, result, round, tournament }) => { [user, result, round, tournament], ); + const mayUnverify = React.useMemo( + () => mayUnverifyResult(user, tournament, round, result), + [user, result, round, tournament], + ); + + const handleUnverifyClick = React.useCallback(async () => { + setUnverifying(true); + try { + await actions.unverifyResult(result); + } catch (e) { + console.error(e); + } + setUnverifying(false); + }, [actions, result]); + const handleVerifyClick = React.useCallback(async () => { setVerifying(true); try { @@ -32,15 +48,26 @@ const Verification = ({ actions, result, round, tournament }) => { const date = result?.verified_at ? new Date(result.verified_at) : null; if (result?.verified_at) { - return
- {result.verified_by ? - - {{date}} - - - : - t('results.verifiedAt', { date }) - } + return
+
+ {result.verified_by ? + + {{date}} + + + : + t('results.verifiedAt', { date }) + } +
+ {mayUnverify ? + + : null}
; } @@ -62,6 +89,7 @@ const Verification = ({ actions, result, round, tournament }) => { Verification.propTypes = { actions: PropTypes.shape({ + unverifyResult: PropTypes.func, verifyResult: PropTypes.func, }), result: PropTypes.shape({ diff --git a/resources/js/helpers/permissions.js b/resources/js/helpers/permissions.js index 9a004c1..e6fc01c 100644 --- a/resources/js/helpers/permissions.js +++ b/resources/js/helpers/permissions.js @@ -221,6 +221,9 @@ export const mayVerifyResult = (user, tournament, round, result) => { return mayVerifyResults(user, tournament) && user && result && user.id !== result.user_id; }; +export const mayUnverifyResults = mayVerifyResults; +export const mayUnverifyResult = mayVerifyResult; + // Twitch export const mayManageTwitchBot = user => isAnyChannelAdmin(user); diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 10797cc..181f88b 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -585,6 +585,7 @@ export default { result: { comment: 'Ergebnis von Runde {{number}} kommentiert: <1>{{comment}}', report: 'Ergebnis von <1>{{time}} bei Runde {{number}} eingetragen', + unverify: 'Verifikation von Ergebnis in Runde {{number}} von {{runner}} (<2>{{time}}) zurückgezogen', verify: 'Ergebnis in Runde {{number}} von {{runner}} (<2>{{time}}) verifiziert', }, round: { @@ -636,6 +637,9 @@ export default { score: 'Punkte', table: 'Tabelle', time: 'Zeit: {{ time }}', + unverify: 'Verifikation entziehen', + unverifyError: 'Fehler beim Entziehen', + unverifySuccess: 'Verifikation entzogen', verification: 'Verifikation', verificationPending: 'Noch nicht verifiziert', verifiedAt: 'Verifiziert am {{ date, L }}', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index 0491e31..3b1f875 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -585,6 +585,7 @@ export default { result: { comment: 'Result of round {{number}} commented: <1>{{comment}}', report: 'Result of <1>{{time}} reported for round {{number}}', + unverify: 'Revoked verification for round {{number}} result of {{runner}} (<2>{{time}})', verify: 'Verified round {{number}} result of {{runner}} (<2>{{time}})', }, round: { @@ -636,6 +637,9 @@ export default { score: 'Score', table: 'Table', time: 'Time: {{ time }}', + unverify: 'Revoke verification', + unverifyError: 'Error revoking verification', + unverifySuccess: 'Verification revoked', verification: 'Verification', verificationPending: 'Pending verification', verifiedAt: 'Verified on {{ date, L }}', diff --git a/resources/js/pages/Tournament.jsx b/resources/js/pages/Tournament.jsx index d1ffaae..54aa92f 100644 --- a/resources/js/pages/Tournament.jsx +++ b/resources/js/pages/Tournament.jsx @@ -14,6 +14,7 @@ import Dialog from '../components/techniques/Dialog'; import Detail from '../components/tournament/Detail'; import { mayEditContent, + mayUnverifyResults, mayVerifyResults, } from '../helpers/permissions'; import { getTranslation } from '../helpers/Technique'; @@ -173,6 +174,16 @@ export const Component = () => { } }, [id, t]); + const unverifyResult = React.useCallback(async (result) => { + try { + const response = await axios.post(`/api/results/${result.id}/unverify`); + toastr.success(t('results.unverifySuccess')); + setTournament(tournament => patchResult(tournament, response.data)); + } catch (e) { + toastr.error(t('results.unverifyError', e)); + } + }); + const verifyResult = React.useCallback(async (result) => { try { const response = await axios.post(`/api/results/${result.id}/verify`); @@ -191,6 +202,7 @@ export const Component = () => { } : null, moreRounds: canLoadMoreRounds(tournament) ? moreRounds : null, selfAssignGroups, + unverifyResult: mayUnverifyResults(user, tournament) ? unverifyResult : null, verifyResult: mayVerifyResults(user, tournament) ? verifyResult : null, }), [addRound, moreRounds, selfAssignGroups, tournament, user, verifyResult]); diff --git a/routes/api.php b/routes/api.php index b599d4c..aacbf49 100644 --- a/routes/api.php +++ b/routes/api.php @@ -86,6 +86,7 @@ Route::get('protocol/{tournament}/{round}', 'App\Http\Controllers\ProtocolContro Route::post('results', 'App\Http\Controllers\ResultController@create'); Route::post('results/{result}/verify', 'App\Http\Controllers\ResultController@verify'); +Route::post('results/{result}/unverify', 'App\Http\Controllers\ResultController@unverify'); Route::post('rounds', 'App\Http\Controllers\RoundController@create'); Route::put('rounds/{round}', 'App\Http\Controllers\RoundController@update'); -- 2.47.3