Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,45 @@ The frontend uses Next.js with React:
- Role-based access control (RBAC) with CASL
- Support for TOTP 2FA

## Permission System Architecture

The application features a centralized Role-Based Access Control (RBAC) system for fact-checking workflows:

### Centralized Permission System
- **Location**: `src/machines/reviewTask/permissions.ts`
- **Hook**: `useReviewTaskPermissions()` in `src/machines/reviewTask/usePermissions.ts`
- **Purpose**: Single source of truth for all permission logic across review states

### Permission Types
- **State Access**: `canAccessState` - who can access each workflow state
- **Editor Permissions**: `canViewEditor`, `canEditEditor`, `editorReadonly` - comprehensive editor control
- **Form Permissions**: `canSubmitActions`, `canSelectUsers`, `formType` - dynamic form behavior
- **UI Control**: `showForm`, `showSaveDraftButton`, `canSaveDraft` - fine-grained UI visibility

### Debug Mode (Development Only)
For testing different user scenarios without multiple accounts:

```typescript
// Enable in browser console or through debug tools
debugInfo.set({
DEBUG_ROLE: Roles.FactChecker, // Override user role
DEBUG_ASSIGNMENT_TYPE: DebugAssignmentType.Assignee // Override assignment
});
```

**Debug Assignment Types**:
- `DebugAssignmentType.None` - Use natural assignments
- `DebugAssignmentType.Assignee` - Test as assigned fact-checker
- `DebugAssignmentType.Reviewer` - Test as assigned reviewer
- `DebugAssignmentType.CrossChecker` - Test as assigned cross-checker

**Debug Roles**: Admin, SuperAdmin, FactChecker, Reviewer, Regular

### State Machine Workflow
Review tasks follow these states with proper RBAC enforcement:
- `unassigned` → `assigned` → `reported` → `reviewing/crossChecking` → `published`
- Each state has specific permissions for different user roles and assignments

## Key Technical Considerations

1. **TypeScript**: The project uses TypeScript but with `strict: false`. Be cautious about type safety.
Expand Down
4 changes: 3 additions & 1 deletion public/locales/en/claimReview.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@
"unhideError": "Error while unhide personality",
"hiddenPersonalityAvatarTooltip": "Hidden personality",
"crossCheckingClassification": "Cross classification",
"crossCheckingComments": "Comments"
"crossCheckingComments": "Comments",
"factCheckingInProgress": "Fact-checking in progress",
"factCheckingInfoMessage": "This content is currently being fact-checked by our team. The report will be available once the review process is complete."
}
8 changes: 7 additions & 1 deletion public/locales/en/claimReviewForm.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"reviewerPlaceholder": "Select a user",
"crossCheckerLabel": "Select a reviewer for cross-ckecking",
"crossCheckerPlaceholder": "Select a user",
"viewOnlyMode": "You are viewing this report in read-only mode. The content is visible but cannot be edited.",
"crossCheckingCommentLabel": "Cross-checking comment",
"crossCheckingCommentPlaceholder": "Insert cross-checking comment",
"crossCheckingClassificationLabel": "Review this report",
Expand All @@ -56,5 +57,10 @@
"isSensitiveLabel": "Sensitve content",
"isSensitive": "This verification request contains sensitve content",
"viewPreview": "View preview",
"hidePreview": "Hide preview"
"hidePreview": "Hide preview",
"assigneeChipLabel": "Assignee",
"reviewerChipLabel": "Reviewer",
"crossCheckerChipLabel": "Cross-Checker",
"validationErrorTitle": "Please fix the following errors",
"validationErrorDescription": "Some required fields are missing or invalid. Please review the highlighted fields below."
}
3 changes: 2 additions & 1 deletion public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"captchaLabel": "Verification",
"change": "Change",
"approve": "Approve",
"reject": "Reject"
"reject": "Reject",
"viewOnly": "View Only"
}
23 changes: 22 additions & 1 deletion public/locales/en/reviewTask.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"SEND_TO_REVIEW_ERROR": "Error when sending for revision",
"ADD_REJECTION_COMMENT_SUCCESS": "Rejection saved succesfully",
"ADD_REJECTION_COMMENT_ERROR": "Error when saving the rejection",
"CONFIRM_REJECTION_SUCCESS": "Rejection confirmed successfully",
"CONFIRM_REJECTION_ERROR": "Error when confirming the rejection",
"PARTIAL_REVIEW_SUCCESS": "Summary saved succesfully",
"PARTIAL_REVIEW_ERROR": "Error when saving the summary",
"FULL_REVIEW_SUCCESS": "Summary saved succesfully",
Expand All @@ -33,6 +35,7 @@
"RESET_ERROR": "It was not possible to reset",

"ADD_REJECTION_COMMENT": "Send comment",
"CONFIRM_REJECTION": "Confirm rejection",
"ASSIGN_USER": "Assign",
"ASSIGN_REQUEST": "Assign",
"FINISH_REPORT": "Finish Report",
Expand Down Expand Up @@ -68,5 +71,23 @@
"invalidReviewerMessage": "The reviewer must not be any user assigned to this task",
"seeFullPage": "See full page",

"sentenceInfo": "This sentence is being checked by AletheiaFact."
"sentenceInfo": "This sentence is being checked by AletheiaFact.",

"recaptchaModalTitle": "Please complete the captcha to continue",
"recaptchaModalConfirm": "Confirm",
"recaptchaModalCancel": "Cancel",

"unassigned": "Unassigned",
"cross-checking": "Cross-checking",
"selectReviewer": "Selecting reviewer",
"selectCrossChecker": "Selecting cross-checker",
"addCommentCrossChecking": "Adding comment",
"rejected": "Rejected",

"stageAssign": "Assign",
"stageAssigned": "Assigned",
"stageDraft": "Draft",
"stageReview": "Review",
"stageApproval": "Approval",
"stagePublished": "Published"
}
4 changes: 3 additions & 1 deletion public/locales/pt/claimReview.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@
"unhideSuccess": "Relatório público com sucesso",
"unhideError": "Erro ao mostrar o relatório",
"crossCheckingClassification": "Etiqueta cross-check",
"crossCheckingComments": "Comentários"
"crossCheckingComments": "Comentários",
"factCheckingInProgress": "Checagem em andamento",
"factCheckingInfoMessage": "Este conteúdo está sendo checado pela nossa equipe. O relatório estará disponível quando o processo de revisão for concluído."
}
8 changes: 7 additions & 1 deletion public/locales/pt/claimReviewForm.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"reviewerPlaceholder": "Selecione um usuário",
"crossCheckerLabel": "Selecione um revisor para o cross-ckecking",
"crossCheckerPlaceholder": "Selecione um usuário",
"viewOnlyMode": "Você está visualizando este relatório em modo somente leitura. O conteúdo está visível mas não pode ser editado.",
"crossCheckingCommentLabel": "Comentário do cross-checking",
"crossCheckingCommentPlaceholder": "Insira o comentário do cross-checking",
"crossCheckingClassificationLabel": "Revise esse relatório",
Expand All @@ -56,5 +57,10 @@
"isSensitiveLabel": "Conteúdo confidencial",
"isSensitive": "Esta denúncia contém conteúdo confidencial",
"viewPreview": "Visualizar pré-visualização",
"hidePreview": "Ocultar pré-visualização"
"hidePreview": "Ocultar pré-visualização",
"assigneeChipLabel": "Responsável",
"reviewerChipLabel": "Revisor",
"crossCheckerChipLabel": "Cross-Checker",
"validationErrorTitle": "Por favor, corrija os seguintes erros",
"validationErrorDescription": "Alguns campos obrigatórios estão ausentes ou inválidos. Revise os campos destacados abaixo."
}
3 changes: 2 additions & 1 deletion public/locales/pt/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"captchaLabel": "Verificação",
"change": "Mudar",
"approve": "Aprovar",
"reject": "Rejeitar"
"reject": "Rejeitar",
"viewOnly": "Somente leitura"
}
23 changes: 22 additions & 1 deletion public/locales/pt/reviewTask.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"SEND_TO_REVIEW_ERROR": "Erro ao enviar para revisão",
"ADD_REJECTION_COMMENT_SUCCESS": "Rejeição salva com sucesso",
"ADD_REJECTION_COMMENT_ERROR": "Erro ao salvar rejeição",
"CONFIRM_REJECTION_SUCCESS": "Rejeição confirmada com sucesso",
"CONFIRM_REJECTION_ERROR": "Erro ao confirmar a rejeição",
"PARTIAL_REVIEW_SUCCESS": "Resumo salvo com sucesso",
"PARTIAL_REVIEW_ERROR": "Erro ao salvar resumo",
"FULL_REVIEW_SUCCESS": "Resumo salvo com sucesso",
Expand All @@ -33,6 +35,7 @@
"RESET_ERROR": "Não foi possível reiniciar o fluxo",

"ADD_REJECTION_COMMENT": "Enviar comentário",
"CONFIRM_REJECTION": "Confirmar rejeição",
"ASSIGN_USER": "Atribuir",
"ASSIGN_REQUEST": "Atribuir",
"FINISH_REPORT": "Concluir Relatório",
Expand Down Expand Up @@ -68,5 +71,23 @@
"invalidReviewerMessage": "O revisor não pode ser um usuário atribuído a essa checagem.",
"seeFullPage": "Ver página completa",

"sentenceInfo": "Esta sentença está sendo checada pela equipe AletheiaFact."
"sentenceInfo": "Esta sentença está sendo checada pela equipe AletheiaFact.",

"recaptchaModalTitle": "Por favor, complete o captcha para continuar",
"recaptchaModalConfirm": "Confirmar",
"recaptchaModalCancel": "Cancelar",

"unassigned": "Não atribuído",
"cross-checking": "Cross-checking",
"selectReviewer": "Selecionando revisor",
"selectCrossChecker": "Selecionando cross-checker",
"addCommentCrossChecking": "Adicionando comentário",
"rejected": "Rejeitado",

"stageAssign": "Atribuição",
"stageAssigned": "Atribuído",
"stageDraft": "Rascunho",
"stageReview": "Revisão",
"stageApproval": "Aprovação",
"stagePublished": "Publicado"
}
12 changes: 9 additions & 3 deletions server/review-task/dto/create-review-task.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { IsEnum, IsNotEmpty, IsObject, IsString } from "class-validator";
import {
IsEnum,
IsNotEmpty,
IsObject,
IsOptional,
IsString,
} from "class-validator";

import { ClassificationEnum } from "../../claim-review/dto/create-claim-review.dto";
import { Personality } from "../../personality/mongo/schemas/personality.schema";
Expand Down Expand Up @@ -63,10 +69,10 @@ export class CreateReviewTaskDTO {
@ApiProperty()
reportModel: string;

@IsNotEmpty()
@IsOptional()
@IsString()
@ApiProperty()
recaptcha: string;
recaptcha?: string;

@IsString()
@ApiProperty()
Expand Down
58 changes: 58 additions & 0 deletions server/review-task/dto/save-draft.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
IsNotEmpty,
IsObject,
IsOptional,
IsBoolean,
IsString,
IsArray,
} from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { ReviewTaskMachineContextReviewData } from "./create-review-task.dto";

class SaveDraftReview {
@IsOptional()
@IsArray()
@ApiProperty()
usersId?: string[];

@IsOptional()
@IsString()
@ApiProperty()
personality?: string;

@IsOptional()
@IsBoolean()
@ApiProperty()
isPartialReview?: boolean;

@IsOptional()
@IsString()
@ApiProperty()
targetId?: string;
}

class SaveDraftContext {
@IsOptional()
@IsObject()
@ApiProperty()
reviewData?: Partial<ReviewTaskMachineContextReviewData>;

@IsOptional()
@IsObject()
@ApiProperty()
review?: SaveDraftReview;
}

class SaveDraftMachine {
@IsNotEmpty()
@IsObject()
@ApiProperty()
context: SaveDraftContext;
}

export class SaveDraftDTO {
@IsNotEmpty()
@IsObject()
@ApiProperty()
machine: SaveDraftMachine;
}
29 changes: 24 additions & 5 deletions server/review-task/review-task.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { ReviewTaskService } from "./review-task.service";
import { CreateReviewTaskDTO } from "./dto/create-review-task.dto";
import { UpdateReviewTaskDTO } from "./dto/update-review-task.dto";
import { SaveDraftDTO } from "./dto/save-draft.dto";
import { CaptchaService } from "../captcha/captcha.service";
import { parse } from "url";
import type { Request, Response } from "express";
Expand Down Expand Up @@ -86,15 +87,33 @@ export class ReviewTaskController {
@Post("api/reviewtask")
@Header("Cache-Control", "no-cache")
async create(@Body() createReviewTask: CreateReviewTaskDTO) {
const validateCaptcha = await this.captchaService.validate(
createReviewTask.recaptcha
);
if (!validateCaptcha) {
throw new Error("Error validating captcha");
if (createReviewTask.recaptcha) {
const validateCaptcha = await this.captchaService.validate(
createReviewTask.recaptcha
);
if (!validateCaptcha) {
throw new Error("Error validating captcha");
}
}
return this.reviewTaskService.create(createReviewTask);
}

@ApiTags("review-task")
@Put("api/reviewtask/save-draft/:data_hash")
@Header("Cache-Control", "no-cache")
async saveDraft(
@Param("data_hash") data_hash: string,
@Body() saveDraftBody: SaveDraftDTO
) {
const reviewTask = await this.reviewTaskService.getReviewTaskByDataHash(
data_hash
);
if (!reviewTask) {
throw new Error("Review task not found");
}
return this.reviewTaskService.saveDraft(data_hash, saveDraftBody);
}

@ApiTags("review-task")
@Put("api/reviewtask/:data_hash")
@Header("Cache-Control", "no-cache")
Expand Down
41 changes: 41 additions & 0 deletions server/review-task/review-task.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ReviewTask, ReviewTaskDocument } from "./schemas/review-task.schema";
import { InjectModel } from "@nestjs/mongoose";
import { CreateReviewTaskDTO, Machine } from "./dto/create-review-task.dto";
import { UpdateReviewTaskDTO } from "./dto/update-review-task.dto";
import { SaveDraftDTO } from "./dto/save-draft.dto";
import { ClaimReviewService } from "../claim-review/claim-review.service";
import { ReportService } from "../report/report.service";
import { HistoryType, TargetModel } from "../history/schema/history.schema";
Expand Down Expand Up @@ -593,6 +594,46 @@ export class ReviewTaskService {
);
}

async saveDraft(data_hash: string, saveDraftBody: SaveDraftDTO) {
const reviewTask = await this.getReviewTaskByDataHash(data_hash);

if (!reviewTask) {
return null;
}

const setFields: Record<string, any> = {};

// Merge reviewData fields individually to preserve existing data
if (saveDraftBody.machine.context.reviewData) {
const draftReviewData = saveDraftBody.machine.context.reviewData;
for (const [key, value] of Object.entries(draftReviewData)) {
if (value !== undefined) {
setFields[`machine.context.reviewData.${key}`] = value;
}
}
}

// Merge review fields individually to preserve existing data
if (saveDraftBody.machine.context.review) {
const draftReview = saveDraftBody.machine.context.review;
for (const [key, value] of Object.entries(draftReview)) {
if (value !== undefined) {
setFields[`machine.context.review.${key}`] = value;
}
}
}

if (Object.keys(setFields).length === 0) {
return reviewTask;
}

return this.ReviewTaskModel.findOneAndUpdate(
{ data_hash },
{ $set: setFields },
{ new: true }
);
}

getReviewTaskByDataHash(data_hash: string) {
const commentPopulation = [
{
Expand Down
Loading
Loading