Skip to main content

Task Breakdown: Qontak Launchpad — Audit Logs for Permission Changes

Decisions recorded​

ItemDecision
Pagination shapemeta.total (with COUNT query)
crs_edit.go team notificationAlready done — no task needed
Slicing modeHorizontal
Blocked tasksInclude all

Effort Summary​

Phase / AreaFE daysBE daysQA daysTotal
Phase 1 — UI (mocked)3—0.53.5
Phase 2 — API integration0.5516.5
Grand total3.551.510

Confidence: medium→high. Both recon unknowns resolved: (1) canCreateUsers confirmed at useManagePermission.ts:53 as the correct permission gate. (2) Nav items are API-driven via qontak-unified-component TopNav — FE change is spaLinks only; actual menu entry requires platform team config coordination (not in estimate).


Phase 1 — UI (APIs mocked)​

Task 1.1: [FE] Audit Logs page — permission guard, nav tab, filter bar, data table (AL-S01)​

A user with usman_users_manage can navigate to the Audit Logs tab and see a filterable, paginated table of their company's permission-change events.

Status: ⚠️ Partially blocked — real GET /iag/v1/audit-logs endpoint does not exist yet; composable uses a mocked stub. Real API call wired in Task 2.5.

Design reference: n/a — no Figma link in RFC. UI contract specified in RFC §2.A.

What to build​

New "Audit Logs" section under User Management: page shell, route guard (redirect to /unauthorized if no usman_users_manage), spaLinks addition for the new route, filter bar (object type, action, date range, actor SSO ID), data table with pagination. All API responses are mocked in this task.

Implementation Plan​

ActionFileWhat changes
createapp/features/user_management/composables/useAuditLogs.tsPinia store (useAuditLogsStore) — filter state, mocked fetch, pagination state
createapp/features/user_management/composables/useAuditLogs.spec.tsVitest tests: filter changes, pagination, loading/error states
createapp/features/user_management/audit_logs/index/views/AuditLogFilters.vueFilter bar: object type dropdown, action dropdown, date range pickers, actor SSO ID input
createapp/features/user_management/audit_logs/index/views/AuditLogTable.vueData table: Date/Time, Actor, Object Type, Action, Target; skeleton rows; empty state; MpPagination
createapp/pages/user_management/audit-logs/index.vueThin page shell — imports views, definePageMeta({ layout: "default" })
extendapp/middleware/permission.global.tsGuard: path /user_management/audit-logs + !canCreateUsers → navigateTo('/unauthorized')
extendapp/layouts/default.vue:215Add "/user_management/audit-logs" to spaLinks array
extendapp/common/components/layouts/QontakOneSidebar.vueSame spaLinks addition for QontakOne layout
⚠ configqontak-unified-component TopNav menu APIActual "Audit Logs" nav item is server-driven — coordinate with platform team for menu config entry (outside this repo)

Permission property confirmed (useManagePermission.ts:53) — usersPermissions.canManageUsers does not exist. Correct property is usersPermissions.canCreateUsers; it gates on usman_users_manage.

Nav is API-driven — TopNav from qontak-unified-component fetches menu items from server config. The FE change is only spaLinks. Actual menu entry requires platform team coordination.

Implementation Steps​

  1. Explore — Open app/features/user_management/composables/useRoles.ts and useRoles.spec.ts. Note Pinia store structure, how useClient is called, and the vitest mock pattern (vi.hoisted, vi.stubGlobal("useRuntimeConfig", ...)). New store mirrors this exactly.

  2. Write failing tests (red) — Create app/features/user_management/composables/useAuditLogs.spec.ts. Cover: initial state (empty filters, page 1), fetchAuditLogs() loading transitions, filter change resets page to 1, error state. Run pnpm test -- app/features/user_management/composables/useAuditLogs.spec.ts — confirm failure.

  3. Scaffold store — defineStore("usman-audit-logs-store", ...) with refs: filters, data, loading, error, pagination (page, perPage, total).

  4. Wire mocked fetch — Stub response with hardcoded array. Use useClient<AuditLogListResponse> pattern. Add // TODO Task 2.5: replace with real endpoint.

  5. Build AuditLogFilters.vue — Pixel3 MpFlex, MpSelect, MpDatepicker, MpInput, MpButton. Filter changes call fetchAuditLogs().

  6. Build AuditLogTable.vue — Pixel3 table. Skeleton rows on loading, "No audit logs found" on empty, MpPagination consuming pagination.total.

  7. Page shell — Import both views. onMounted(() => useAuditLogsStore().fetchAuditLogs()).

  8. Route guard — permission.global.ts: if path starts with /user_management/audit-logs and !managePermissionStore.usersPermissions.canCreateUsers → navigateTo('/unauthorized').

  9. spaLinks — Add "/user_management/audit-logs" to spaLinks in app/layouts/default.vue:215 and app/common/components/layouts/QontakOneSidebar.vue. Coordinate with platform team for TopNav menu config entry.

  10. Go green + gate — pnpm test passes → pnpm lint && pnpm build.

Acceptance Criteria​

  • /user_management/audit-logs loads for user with usman_users_manage; redirects to /unauthorized without it
  • "Audit Logs" nav item visible only when canCreateUsers is true (pending platform team menu config)
  • Filter bar: object type, action, date range, actor SSO ID — all controls render
  • Changing any filter resets page to 1 and re-fetches
  • Table: Date/Time, Actor, Object Type, Action, Target columns
  • Loading → skeleton rows; empty → "No audit logs found"
  • Pagination functional; page changes trigger re-fetch
  • pnpm test passes; pnpm build succeeds
  • (Pending Task 2.5) Real API call replaces mock stub

Test Strategy​

Vitest tests on useAuditLogsStore. Stub useClient via vi.hoisted + vi.stubGlobal following useRoles.spec.ts pattern. Assert: loading transitions, mock response maps to data, filter changes pass correct query params to useClient.

Effort Estimate​

DisciplineDays
Frontend3
Backend—
QA0.5
Total3.5

Assumptions: Pixel3 MpTable/MpSelect/MpDatepicker work with Nuxt 4. Reuses useClient + Pinia pattern from useRoles.ts. spaLinks files confirmed. Platform team nav config coordination not included in estimate.

Run to verify​

pnpm test -- app/features/user_management/composables/useAuditLogs.spec.ts && pnpm lint && pnpm build

Depends on​

  • None (mocked API; fully independent of BE work)

Phase 2 — API Integration​

Task 2.1: [BE] Write-side audit gap fixes — crs_edit bug + UpdatePermissionLevel + BackfillPermission (AL-S02, AL-S03, AL-S04)​

Permission level updates and backfill operations produce audit log entries; role edits emit update (not create) as the action type.

Status: ✅ Actionable

Design reference: n/a — BE only

What to build​

Three targeted changes in the roles service layer: fix ActionAuditCreate → ActionAuditUpdate at crs_edit.go:247; add CreateAuditLog after the primary DB write in UpdatePermissionLevel; add CreateAuditLog after the primary DB write in BackfillPermission.

Implementation Plan​

ActionFileWhat changes
fixinternal/app/service/roles/crs_edit.go:247consts.ActionAuditCreate → consts.ActionAuditUpdate
extendinternal/app/service/roles/update_permission_level.goAdd s.auditLogger.CreateAuditLog(...) after primary DB write; swallow audit error with Error log
extendinternal/app/service/roles/backfill_permission.goSame audit call pattern after primary mutation
extendinternal/app/service/roles/crs_edit_test.goUpdate assertion: action_audit == "update"
extend/createinternal/app/service/roles/update_permission_level_test.goAdd TestUpdatePermissionLevel_AuditsOnSuccess + TestUpdatePermissionLevel_NoAuditOnFailure
extend/createinternal/app/service/roles/backfill_permission_test.goAdd TestBackfillPermission_AuditsOnSuccess + TestBackfillPermission_NoAuditOnFailure

Implementation Steps​

  1. Explore — Open internal/app/service/roles/crs_edit.go:240–260 and crs_edit_test.go. Note how s.auditLogger.CreateAuditLog is called (arg order: ctx, repo, objectAudit, actionAudit, actorSsoID, objectID, data) and how the existing test captures the mock call. Open roles/main.go to confirm s.auditLogger wired in constructor.

  2. Write failing tests (red) — Change crs_edit_test.go assertion to ActionAuditUpdate (will fail now). Add TestUpdatePermissionLevel_AuditsOnSuccess using MockAuditLogger; assert called once with consts.ObjectAuditRoleHasFeaturePermissions + consts.ActionAuditUpdate. Add NoAuditOnFailure variant: mock primary DB failure; assert CreateAuditLog NOT called. Mirror for backfill. Run go test -race ./internal/app/service/roles/... — confirm failures.

  3. Fix crs_edit.go:247 — consts.ActionAuditCreate → consts.ActionAuditUpdate.

  4. Extend update_permission_level.go — After confirmed successful primary DB write:

    if auditErr := s.auditLogger.CreateAuditLog(ctx, s.repo,
    consts.ObjectAuditRoleHasFeaturePermissions, consts.ActionAuditUpdate,
    req.ActorSsoID, req.RoleID, req); auditErr != nil {
    slog.ErrorContext(ctx, "audit log failed", slog.Any("error", auditErr))
    }

    Audit failure must NOT propagate to caller.

  5. Extend backfill_permission.go — Same pattern. Inspect file to confirm which ObjectAudit constant applies.

  6. Go green — go test -race ./internal/app/service/roles/...

  7. Quality gate — go fmt ./... && go vet ./... && make lint && make sec

Acceptance Criteria​

  • crs_edit_test.go passes with action_audit == "update" assertion
  • TestUpdatePermissionLevel_AuditsOnSuccess: CreateAuditLog called once after success
  • TestUpdatePermissionLevel_NoAuditOnFailure: CreateAuditLog NOT called when primary mutation fails
  • Both tests mirrored for BackfillPermission
  • Audit failure does NOT propagate to caller (Error log only)
  • go test -race ./internal/app/service/roles/... passes
  • make lint && make sec clean

Test Strategy​

Use mockery-generated MockAuditLogger. Success tests: assert CreateAuditLog called once with correct constants. Failure tests: configure primary DB mock to fail; assert CreateAuditLog NOT called.

Effort Estimate​

DisciplineDays
Frontend—
Backend1
QA0.5
Total1.5

Assumptions: MockAuditLogger already generated by mockery. s.auditLogger wired in roles/main.go constructor (confirmed by recon).

Run to verify​

go test -race ./internal/app/service/roles/... && make lint

Depends on​

  • None (existing service layer; fully independent)

Task 2.2: [BE] DB layer — migration, enriched SQL query, sqlc generate, IStore extension (AL-S01)​

The repository exposes GetUsmanAuditLogsEnriched (SELECT with LEFT JOINs) and GetUsmanAuditLogsEnrichedCount (COUNT for meta.total), both company-scoped via non-nullable $1.

Status: ✅ Actionable

Design reference: n/a — BE only

What to build​

Four sequential steps: create the created_at index migration, add both new queries to the SQL file, run sqlc generate, sync generated output to internal/app/repository/.

Implementation Plan​

ActionFileWhat changes
createdb/migration/<ts>_add_audit_logs_created_at_index.up.sqlCREATE INDEX IF NOT EXISTS idx_usman_audit_logs_created_at ON usman_audit_logs (created_at DESC)
createdb/migration/<ts>_add_audit_logs_created_at_index.down.sqlDROP INDEX IF EXISTS idx_usman_audit_logs_created_at
extenddb/query/usman_audit_log.sqlAppend GetUsmanAuditLogsEnriched :many (RFC §2.3 SQL) + GetUsmanAuditLogsEnrichedCount :one (same WHERE, SELECT COUNT(*), params $1–$7 only)
regeneratedb/sqlc/sqlc generate — do not edit manually
extendinternal/app/repository/querier.goAdd both new method signatures to Querier interface
syncinternal/app/repository/usman_audit_log.sql.goCopy generated impl from db/sqlc/ (strip SQLC headers)

Implementation Steps​

  1. Explore — Read db/query/usman_audit_log.sql for query style. Read internal/app/repository/querier.go for interface declaration pattern. Check newest migration filename for timestamp format.

  2. Create migration files — Use timestamp matching existing naming convention.

  3. Add enriched queries — Append both queries to db/query/usman_audit_log.sql. $1 (company_id) is non-nullable in both.

  4. Run sqlc generate:

    sqlc generate

    Confirm GetUsmanAuditLogsEnriched, GetUsmanAuditLogsEnrichedParams, GetUsmanAuditLogsEnrichedRow, GetUsmanAuditLogsEnrichedCount are generated in db/sqlc/.

  5. Sync to repository — Copy from db/sqlc/usman_audit_log.sql.go into internal/app/repository/usman_audit_log.sql.go (remove SQLC headers). Add both method signatures to internal/app/repository/querier.go.

  6. Verify — go build ./... compiles.

Acceptance Criteria​

  • Migration up/down files exist with correct SQL
  • Both new queries in db/query/usman_audit_log.sql; $1 is non-nullable company_id
  • sqlc generate succeeds
  • GetUsmanAuditLogsEnriched + GetUsmanAuditLogsEnrichedCount on Querier interface
  • go build ./... passes

Test Strategy​

No unit test at this layer — sqlc-generated code is trusted as correct. Build pass is the gate.

Effort Estimate​

DisciplineDays
Frontend—
Backend1.5
QA0
Total1.5

Assumptions: sqlc installed and sqlc.yaml configured. Migration timestamp format from most recent file in db/migration/.

Run to verify​

sqlc generate && go build ./...

Depends on​

  • None (DB layer is standalone)

Task 2.3: [BE] Service layer — request/response types + AuditLogService.GetAuditLogs (AL-S01)​

AuditLogService.GetAuditLogs returns company-scoped, paginated audit entries using company_id from context — never from request params.

Status: ✅ Actionable (depends on Task 2.2 for repository types)

Design reference: n/a — BE only

What to build​

Two new type files, extension of audit_log.go with GetAuditLogs method + IAuditLogService interface, and unit tests asserting company scoping.

Implementation Plan​

ActionFileWhat changes
createinternal/pkg/request/audit_log_request.goAuditLogListRequest with 8 query params + validator tags; embeds http.PaginationRequest
createinternal/pkg/response/audit_log_response.goAuditLogEntryResponse (nullable fields as pointers) + AuditLogListResponse{Data, Meta{Page,PerPage,Total}}
extendinternal/app/service/audit_log.goAdd IAuditLogService interface + GetAuditLogs(ctx, req) — reads company_id from CurrentUser context; calls both enriched repo methods
createinternal/app/service/audit_log_test.goTests: CompanyScopedFromContext (primary), FiltersPassedToRepo, RepoError

Implementation Steps​

  1. Explore — Open internal/pkg/request/user_request.go and internal/pkg/response/user_response.go for field naming, JSON tags, and validator tag patterns. Open internal/pkg/http/pagination.go for PaginationRequest and GetLimitOffset().

  2. Write failing tests (red) — Create internal/app/service/audit_log_test.go:

    // TestGetAuditLogs_CompanyScopedFromContext — primary safety assertion
    ctx := context.WithValue(context.Background(), ctxKey.User, CurrentUser{
    User: repository.FindUserBySsoIdRow{CompanyID: testUUID},
    })
    _, _ = svc.GetAuditLogs(ctx, AuditLogListRequest{})
    mockRepo.AssertCalled(t, "GetUsmanAuditLogsEnriched",
    mock.Anything,
    mock.MatchedBy(func(arg GetUsmanAuditLogsEnrichedParams) bool {
    return arg.CompanyID == testUUID
    }),
    )

    Run go test -race ./internal/app/service/... — confirm failure.

  3. Create audit_log_request.go:

    type AuditLogListRequest struct {
    ObjectAudit string `schema:"object_audit" validate:"omitempty,oneof=user company_role teams role user_role role_has_feature_permissions role_has_data_permissions"`
    ObjectID *uuid.UUID `schema:"object_id" validate:"omitempty,uuid"`
    ActionAudit string `schema:"action_audit" validate:"omitempty,oneof=create update destroy transfer_data"`
    ActorSsoID string `schema:"actor_sso_id"`
    From *time.Time `schema:"from"`
    To *time.Time `schema:"to"`
    http.PaginationRequest
    }
  4. Create audit_log_response.go — AuditLogEntryResponse with all fields from RFC §2.4 (nullable as pointers). AuditLogListResponse{Data []AuditLogEntryResponse, Meta struct{Page, PerPage, Total int}}.

  5. Extend audit_log.go — Add IAuditLogService interface. Implement GetAuditLogs: read currentUser from context → extract CompanyID → call GetUsmanAuditLogsEnriched + GetUsmanAuditLogsEnrichedCount → map to response.

  6. Go green — go test -race ./internal/app/service/...

  7. Quality gate — go fmt ./... && go vet ./... && make lint

Acceptance Criteria​

  • TestGetAuditLogs_CompanyScopedFromContext: repo called with company_id from context, never from request
  • TestGetAuditLogs_FiltersPassedToRepo: optional filters correctly forwarded
  • TestGetAuditLogs_RepoError: error propagated
  • meta.total populated via GetUsmanAuditLogsEnrichedCount
  • No PII in slog.InfoContext calls
  • go test -race ./internal/app/service/... passes

Test Strategy​

Mock repository.IStore via mockery. Primary assertion: TestGetAuditLogs_CompanyScopedFromContext verifies company_id always originates from context, not the request struct.

Effort Estimate​

DisciplineDays
Frontend—
Backend1
QA0
Total1

Assumptions: middleware.GetCurrentUser(ctx) helper exists (verify in sso_auth.go; fallback: ctx.Value(ctxKey.User)). IAuditLogService interface defined here — Task 2.4 depends on it.

Run to verify​

go test -race ./internal/app/service/... && make lint

Depends on​

  • [Task 2.2] — GetUsmanAuditLogsEnrichedRow type must exist before service compiles

Task 2.4: [BE] AuditLogHandler + permission mapping + route wiring (AL-S01)​

GET /iag/v1/audit-logs with a valid usman_users_manage JWT returns 200 paginated company-scoped audit logs; 401 without JWT; 403 without the permission.

Status: ✅ Actionable

Design reference: n/a — BE only

What to build​

New handler file with List method + Swagger annotation, one-line permission mapping addition, route registration under /iag/v1, Swagger regeneration.

Implementation Plan​

ActionFileWhat changes
createinternal/server/audit_log_handler.goAuditLogHandler struct + List method; Swagger annotation @Router /iag/v1/audit-logs [get]
createinternal/server/audit_log_handler_test.goHandler tests: 200, 400 (invalid UUID), 401, 403
extendinternal/pkg/middleware/permission_check.goAdd "GET /audit-logs": "usman_users_manage" to staticPermissionMappings
extendinternal/server/rest_router.gor.Get("/audit-logs", auditLogHandler.List) inside /iag/v1 authenticated group
regeneratedocs/swagger.json, docs/swagger.yamlmake init after Swagger annotation added

Implementation Steps​

  1. Explore — Open internal/app/handler/role_handler.go for handler struct pattern. Open internal/server/rest_router.go:77–105 for /iag/v1 group structure. Open an existing handler for Swagger annotation format.

  2. Write failing tests (red) — Create internal/server/audit_log_handler_test.go. Table:

    • 200: inject CurrentUser in context; mock service returns test data; assert JSON shape
    • 400: object_id=not-a-uuid; assert 400 + GetAuditLogs NOT called
    • 401: no CurrentUser in context
    • 403: permission denied Run go test -race ./internal/server/... — confirm failures.
  3. Create audit_log_handler.go:

    type AuditLogHandler struct {
    auditLogService service.IAuditLogService
    binder binder.IBinder
    }

    List: bind + validate AuditLogListRequest → call GetAuditLogs → return response.

  4. Add Swagger annotation — all 8 query params documented; @Security BearerAuth; @Router /iag/v1/audit-logs [get].

  5. Add permission mapping:

    // internal/pkg/middleware/permission_check.go — staticPermissionMappings:
    "GET /audit-logs": "usman_users_manage",
  6. Register route — r.Get("/audit-logs", auditLogHandler.List) inside /iag/v1 group.

  7. Regenerate Swagger — make init

  8. Go green + gate — go test -race ./internal/server/... → make prepare

Acceptance Criteria​

  • Authorised token → 200 JSON with data array and meta.total
  • object_id=invalid-uuid → 400 INVALID_PARAM; GetAuditLogs not called
  • No JWT → 401
  • Valid JWT, no usman_users_manage → 403
  • slog.InfoContext logs only object_audit + page (no PII)
  • Swagger annotation present; make init succeeds
  • make prepare passes (build + test + lint + sec)

Test Strategy​

httptest.NewRecorder() + httptest.NewRequest() table tests. Mock IAuditLogService via mockery. 200 case: inject CurrentUser in context, assert JSON shape. 400 case: assert GetAuditLogs never called (AssertNotCalled).

Effort Estimate​

DisciplineDays
Frontend—
Backend1.5
QA0.5
Total2

Assumptions: IBinder injection same as existing handlers. IAuditLogService interface from Task 2.3.

Run to verify​

go test -race ./internal/server/... && make prepare

Depends on​

  • [Task 2.3] — IAuditLogService interface + GetAuditLogs method must exist

Task 2.5: [FE] Wire real GET /iag/v1/audit-logs (AL-S01)​

The Audit Logs page fetches live data with correct handling for 403 mid-session and 5xx failures.

Status: ✅ Actionable (depends on Task 2.4 endpoint being deployed)

Design reference: n/a — wiring task

What to build​

Replace the mock stub in useAuditLogsStore with the real useClient call. Update spec to assert correct URL, query params, and error paths.

Implementation Plan​

ActionFileWhat changes
extendapp/features/user_management/composables/useAuditLogs.tsReplace mock stub with real useClient('/iag/v1/audit-logs', ...) + 403 mid-session handler
extendapp/features/user_management/composables/useAuditLogs.spec.tsAssert correct URL + params; add 403 redirect test; add 5xx error test

Implementation Steps​

  1. Explore — Open app/features/user_management/composables/useRoles.ts to confirm exact useClient call pattern.

  2. Replace mock — Find // TODO Task 2.5 comment. Replace with:

    const { data, error } = await useClient<AuditLogListResponse>(
    '/iag/v1/audit-logs',
    { method: 'GET', query: toRaw(filters.value) }
    // company_id NOT included — backend derives from JWT
    )
    if (error.value?.statusCode === 403) {
    return navigateTo('/unauthorized')
    }
  3. Update tests — Assert useClientSpy called with '/iag/v1/audit-logs' and no company_id key. Add: 403 spy → assert navigateTo('/unauthorized'); 5xx spy → assert error ref truthy.

  4. Go green + gate — pnpm test -- ...useAuditLogs.spec.ts → pnpm lint && pnpm build

Acceptance Criteria​

  • useClient called with '/iag/v1/audit-logs'; no company_id in query params
  • 403 mid-session → navigateTo('/unauthorized')
  • 5xx → error state set; toast.notify called
  • pnpm test passes; pnpm build succeeds

Test Strategy​

Update useAuditLogs.spec.ts with vi.hoisted mock pattern. Assert URL and param shape. Add 403/5xx error path tests.

Effort Estimate​

DisciplineDays
Frontend0.5
Backend—
QA0
Total0.5

Assumptions: useClient handles token injection. navigateTo is Nuxt auto-imported.

Run to verify​

pnpm test -- app/features/user_management/composables/useAuditLogs.spec.ts && pnpm lint && pnpm build

Depends on​

  • [Task 2.4] — GET /iag/v1/audit-logs endpoint deployed and reachable

Ordering Rationale​

  • Task 1.1 — start immediately. FE work is fully independent; build entire UI in parallel with BE.
  • Tasks 2.1 and 2.2 — also Day 1, in parallel. Write-side audit fixes (2.1) and DB layer (2.2) share no files; two BE devs start simultaneously.
  • Task 2.3 after 2.2 — service needs GetUsmanAuditLogsEnrichedRow type from sqlc.
  • Task 2.4 after 2.3 — handler needs IAuditLogService interface.
  • Task 2.5 last — FE wiring requires the endpoint deployed.
  • Critical path: 2.2 → 2.3 → 2.4 → 2.5. SQL generation (2.2) is the longest sequential chain — unblock it first.

Skipped Stories​

No stories blocked or excluded. All four are actionable:

StoryTasks
AL-S01: User with usman_users_manage views company audit log1.1, 2.2, 2.3, 2.4, 2.5
AL-S02: UpdatePermissionLevel writes audit entry2.1
AL-S03: BackfillPermission writes audit entry2.1
AL-S04: Role edits emit update action (not create)2.1