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)30.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 + !canCreateUsersnavigateTo('/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-drivenTopNav 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 storedefineStore("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 guardpermission.global.ts: if path starts with /user_management/audit-logs and !managePermissionStore.usersPermissions.canCreateUsersnavigateTo('/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 + gatepnpm 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 ActionAuditCreateActionAuditUpdate 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.ActionAuditCreateconsts.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:247consts.ActionAuditCreateconsts.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 greengo test -race ./internal/app/service/roles/...

  7. Quality gatego 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. Verifygo 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.goAuditLogEntryResponse 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 greengo test -race ./internal/app/service/...

  7. Quality gatego 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 router.Get("/audit-logs", auditLogHandler.List) inside /iag/v1 group.

  7. Regenerate Swaggermake init

  8. Go green + gatego 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 + gatepnpm test -- ...useAuditLogs.spec.tspnpm 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