Lexios / Tutoriais
Módulos

Treinamentos · Referência técnica

Contratos do módulo de treinamentos — endpoints, schema do editor, eventos xAPI e fluxo do participante

Treinamentos · Referência técnica

Visão Geral

Esta referência cobre o contrato técnico do módulo de treinamentos — endpoints, schema do editor (BlockNote + bloco Mux) e o vocabulário de eventos xAPI gravados no LRS interno (xapi_statements). A documentação voltada ao usuário final está em Treinamentos EAD.

Endpoints

Admin (trainings:*)

MétodoRotaPermissão
GET/api/trainingstrainings:view
POST/api/trainingstrainings:create
GET/api/trainings/{id}trainings:view
PUT/api/trainings/{id}trainings:edit
DELETE/api/trainings/{id}trainings:delete
POST/api/trainings/{id}/publishtrainings:edit
POST/api/trainings/{tid}/modulestrainings:edit
PUT/api/trainings/{tid}/modules/{id}trainings:edit
DELETE/api/trainings/{tid}/modules/{id}trainings:edit
PUT/api/trainings/{tid}/modules/reordertrainings:edit
GET/api/trainings/{tid}/modules/{id}/contenttrainings:view
PUT/api/trainings/{tid}/modules/{id}/contenttrainings:edit
PUT/api/trainings/{tid}/modules/{id}/quiztrainings:edit
DELETE/api/trainings/{tid}/modules/{id}/quiztrainings:edit
POST/api/trainings/{tid}/enrollmentstrainings:participants
POST/api/trainings/{tid}/enrollments/send-invitestrainings:participants

Auth do participante

Participantes autenticam pelo mesmo fluxo dos usuários do tenant (/api/auth/*) — recebem um JWT com a permissão participant:access.

MétodoRotaBodyObservação
POST/api/auth/login-with-enrollment-token{ token }Link mágico do convite. Anônimo
POST/api/auth/login{ email, password, authProvider: "email" }Login padrão. Anônimo
POST/api/auth/change-password{ email, tempPassword, newPassword }Troca de senha temporária na primeira entrada

Player do participante (participant:access)

MétodoRotaDescrição
GET/api/participant/trainingsLista todas as matrículas do usuário autenticado
POST/api/participant/trainings/{id}/launchMarca o início da matrícula
GET/api/participant/trainings/{id}Detalhe do treinamento + progresso por módulo
GET/api/participant/trainings/{tid}/modules/{id}Conteúdo do módulo (sem isCorrect/feedback)
POST/api/participant/trainings/{tid}/modules/{id}/completeConclui módulo sem quiz
POST/api/participant/trainings/{tid}/quizzes/{id}/submitSubmete tentativa de quiz
POST/api/participant/trainings/{id}/completeFinaliza o treinamento e emite certificado
POST/api/participant/xapi/statementsPassthrough de eventos xAPI granulares

GET /api/participant/trainings

Resposta: 200 OK com MyTrainingListItem[]. Apenas matrículas do usuário do JWT — não há filtros por query string. Ordenação: matrículas em andamento primeiro, depois por startedAt desc, depois título.

[
  {
    "trainingId": "uuid",
    "title": "LGPD para Times Comerciais",
    "description": "string | null",
    "estimatedWorkloadMinutes": 60,
    "modulesCount": 4,
    "enrollmentId": "uuid",
    "enrollmentStatus": "invited | started | passed | failed | expired",
    "finalScore": 82,
    "startedAt": "2026-05-10T13:42:11Z",
    "completedAt": null,
    "certificateIssuedAt": null,
    "deadlineAt": "2026-06-30T23:59:59Z",
    "expiresAt": "2026-07-01T00:00:00Z"
  }
]

Lista vazia ([]) quando o usuário tem participant:access mas nenhuma matrícula — o frontend renderiza o empty state nesse caso.

Schema do Editor

O conteúdo de cada módulo é persistido como JSON do BlockNote na coluna Content (jsonb) de TrainingModule. O backend não interpreta o documento — apenas armazena e devolve.

O schema do editor é estendido com um bloco custom muxVideo:

BlockNoteSchema.create({
  blockSpecs: { ...defaultBlockSpecs, muxVideo: MuxBlock }
})

Bloco muxVideo

PropriedadeTipoDefaultDescrição
playbackIdstring""Identificador do asset publicado no Mux
titlestring""Título exibido em metadata/relatórios

content: "none" — bloco atômico, sem filhos.

Persistência

  • GET /api/trainings/{tid}/modules/{id}/content retorna { content: string \| null, quiz: QuizPayload \| null }. O campo content é a string JSON inteira do BlockNote.
  • PUT .../content aceita { content: string } — o frontend serializa via JSON.stringify(editor.document).
  • O auto-save dispara após 1500ms sem digitação. Falhas não são reenviadas — o usuário precisa modificar algo de novo para retentar.

Schema do Quiz

Cada módulo pode ter no máximo um quiz. Questões aceitam três tipos:

questionTypeValidação backend
single_choiceResposta correta se selectedOptionIds == [correctId]
multiple_choiceResposta correta se o conjunto de selecionados é igual ao conjunto de corretas
true_falseMesma regra de single_choice (alternativas fixas: Verdadeiro / Falso)

Options é serializado como JSON string:

[
  { "id": "uuid", "label": "...", "isCorrect": true, "feedback": "opcional" }
]

O endpoint GET /api/participant/trainings/.../modules/{id} retira os campos isCorrect e feedback antes de devolver para o participante (ver StripCorrectness em GetModulePlayer/Endpoint.cs).

A nota é calculada como round((points_earned / points_total) * 100). A aprovação compara contra Training.Evaluation.PassingScore.

Vocabulário xAPI

Statements são imutáveis, gravados em xapi_statements. Schema segue o padrão actor · verb · object · result · context.

Verbos suportados

VerboURIOrigem
registeredhttp://adlnet.gov/expapi/verbs/registeredBackend (matrícula)
invitedhttp://id.tincanapi.com/verb/invitedBackend (envio de convite)
launchedhttp://adlnet.gov/expapi/verbs/launchedBackend (/launch)
initializedhttp://adlnet.gov/expapi/verbs/initializedFrontend (mount do módulo)
progressedhttp://adlnet.gov/expapi/verbs/progressedFrontend (Mux 25/50/75/90%)
experiencedhttp://adlnet.gov/expapi/verbs/experiencedFrontend (Mux play/ended)
answeredhttp://adlnet.gov/expapi/verbs/answeredFrontend (por questão, antes do submit)
attemptedhttp://adlnet.gov/expapi/verbs/attemptedBackend (/quizzes/.../submit)
passed / failedhttp://adlnet.gov/expapi/verbs/passed+failedBackend (resultado da tentativa)
completedhttp://adlnet.gov/expapi/verbs/completedBackend (módulo e treinamento)
satisfiedhttp://adlnet.gov/expapi/verbs/satisfiedBackend (certificado emitido)

Endpoint de passthrough do frontend

POST /api/participant/xapi/statements aceita statements granulares emitidos pelo player. Allowlist de verbos: initialized, progressed, experienced, answered. Tipos de objeto aceitos: module, video, question, training.

Body:

{
  "verb": "progressed",
  "objectType": "video",
  "objectId": "<module guid>",
  "objectName": "Aula 1 — LGPD",
  "trainingId": "<training guid>",
  "moduleId": "<module guid>",
  "result": { "duration": "PT45S", "score": { "raw": 45, "min": 0, "max": 180, "scaled": 0.25 } },
  "extras": { "muxPlaybackId": "...", "milestone": 25, "blockId": "..." }
}

O backend valida que o objectId pertence a um treinamento em que o participante está matriculado, anexa trainingId/enrollmentId/moduleId ao Context e grava como XApiStatement. Verbos fora do allowlist retornam 400. Tipos de objeto inválidos retornam 400. Objetos não-pertencentes ao treinamento retornam 404.

Granularidade de tracking de vídeo

O bloco MuxBlock em modo read-only registra:

EventoVerboQuando
playexperienced (phase: started)Primeiro play do vídeo
progressprogressedAo cruzar 25%, 50%, 75% e 90% (não-repetível por sessão)
endedexperienced (phase: ended, result.completion: true)Vídeo terminou

Cada evento carrega muxPlaybackId no extras para correlação posterior com o painel do Mux.

Autenticação do Participante

O participante é um User regular do tenant com uma role que carrega a permissão participant:access. O preset ColaboradorPermissions (em Configs/RolePresets.cs) entrega exatamente essa única permissão.

JWT emitido pelo mesmo fluxo de /api/auth/* dos demais usuários. Claims relevantes:

ClaimOrigem
tenant_idTenant dono do treinamento
user_idUsers.Id
emailUsers.Email
permissionsLista da role — colaborador tem apenas participant:access

Expiração do JWT: 8 horas. O AccessToken da matrícula (token de link mágico) tem expiração própria em TrainingEnrollment.ExpiresAt.

participant:access está em Permissions.All e pode ser atribuída a qualquer role do tenant — endpoints /api/participant/* exigem essa permissão sem checagem adicional de role.

Erros & Edge Cases

CenárioEndpointResposta
Token de matrícula expiradoPOST /participant-auth/token400 com mensagem de expiração
Login com senha mas sem matrículas ativasPOST /participant-auth/login400 "Nenhuma matrícula ativa…"
Tentar concluir módulo com quizPOST .../modules/{id}/complete400 "Este módulo possui quiz"
Submeter quiz já aprovadoPOST .../quizzes/{id}/submit400 "Quiz já foi aprovado"
Submeter quiz com MaxAttempts atingidoidem400 "Limite de tentativas atingido"
AllowRetake = false e segunda tentativaidem400 "Esta avaliação não permite retentativas"
Concluir treinamento com módulos pendentesPOST .../complete400 "Existem N módulo(s) não concluído(s)"
Verbo xAPI fora do allowlistPOST /participant/xapi/statements400 "Verbo não permitido"

Configuração

ChaveDefaultUso
App:TrainingPortalUrlhttps://lexios.com.brBase do link de acesso enviado por e-mail ({base}/treinamento/acesso?token=...)
App:FrontendUrlFallback caso TrainingPortalUrl esteja ausente

Em desenvolvimento local, ambos apontam para http://localhost:3000 (ver appsettings.Development.json).

Nesta página