Skip to content

feat: add FHIR resource ID handling in session middleware and authentication#304

Open
gilanglahat22 wants to merge 9 commits intokonsulin-care:developfrom
gilanglahat22:fix/use-FHIR-bundle
Open

feat: add FHIR resource ID handling in session middleware and authentication#304
gilanglahat22 wants to merge 9 commits intokonsulin-care:developfrom
gilanglahat22:fix/use-FHIR-bundle

Conversation

@gilanglahat22
Copy link

@gilanglahat22 gilanglahat22 commented Feb 4, 2026

Implementation Solutions

This document describes the solution implementations for the following issues in Konsulin API.


Issue 1: Enrich SuperTokens Profile Metadata with Roles and FHIR Resource IDs

Problem

SuperTokens profiles did not provide sufficient metadata for direct authorization checks. Whenever resource ownership validation was needed, the system had to perform additional queries to the database or FHIR server, causing overhead and latency.

Solution

Profile metadata was enriched with:

  1. Roles: Array of assigned roles (Patient, Practitioner, Clinic Admin, Researcher, etc.)
  2. FHIR Resource ID: Formatted string such as Patient/{ID}, Practitioner/{ID}, or Person/{ID} based on the user’s primary role

This metadata is embedded in the SuperTokens access token payload so it can be read without extra queries.

Implementation Details

1. Access Token Payload Enrichment

File: internal/app/services/core/auth/auth_supertoken_impl.go

New constant:

const (
    supertokenAccessTokenPayloadRolesKey       = "st-role"
    supertokenAccessTokenPayloadRolesValueKey  = "v"
    supertokenAccessTokenPayloadFhirResourceId = "fhirResourceId"  // New
)

New helper: getFhirResourceIdForUser(ctx, userID, roles) (string, error)
It derives the FHIR resource ID from the user’s roles with this priority:

  • PractitionerPractitioner/{ID} (highest)
  • PatientPatient/{ID}
  • OtherwisePerson/{ID}

Session creation change: The CreateNewSession override adds fhirResourceId to the access token payload by calling getFhirResourceIdForUser() and setting accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId].

2. Middleware Enhancement

File: internal/app/delivery/http/middlewares/session.go

New constants: keyFHIRResourceId, supertokenAccessTokenPayloadFhirResourceId

SessionOptional middleware: Reads fhirResourceId from the access token payload and stores it in the request context (keyFHIRResourceId, CONTEXT_FHIR_RESOURCE_ID).

3. Context Constant

File: internal/pkg/constvars/internal_app.go

const CONTEXT_FHIR_RESOURCE_ID ContextKey = "fhir_resource_id"

Access Token Payload Shape

{
  "st-role": {
    "v": ["Patient", "Practitioner"]
  },
  "fhirResourceId": "Practitioner/abc123"
}

Benefits

  • Fewer queries to DB/FHIR for resource ID; authorization can use the token.
  • Metadata available on every request; middleware can read roles and FHIR resource ID from context.
  • FHIR resource ID stays in sync with roles and is refreshed on login/refresh.

Usage (middleware or handler)

fhirResourceId, _ := ctx.Value(constvars.CONTEXT_FHIR_RESOURCE_ID).(string)
roles, _ := ctx.Value(keyRoles).([]string)

if strings.HasPrefix(fhirResourceId, "Practitioner/") {
    practitionerID := strings.TrimPrefix(fhirResourceId, "Practitioner/")
    // Use practitionerID for validation
}

Files Modified

  1. internal/app/services/core/auth/auth_supertoken_impl.go – constant, getFhirResourceIdForUser(), CreateNewSession override
  2. internal/app/delivery/http/middlewares/session.go – constants, SessionOptional reading/storing fhirResourceId
  3. internal/pkg/constvars/internal_app.goCONTEXT_FHIR_RESOURCE_ID

Testing

  • Unit: getFhirResourceIdForUser() for different role combinations and priority (Practitioner > Patient > Person).
  • Integration: Access token payload after login; context values in middleware.
  • E2E: API calls with the new token metadata and authorization checks.

Issue 2: Use FHIR Bundle for Batch PractitionerRole Availability Updates

Problem

The PractitionerAvailabilityEditor updated multiple PractitionerRole resources one-by-one in a loop. If one update failed mid-batch, earlier updates were already persisted, leading to an inconsistent state that was hard to recover.

Solution

The availability update flow was refactored to use a FHIR Bundle transaction: the client sends one request with all PractitionerRole updates in a Bundle, and the backend processes it atomically (all-or-nothing). The backend already had the infrastructure to support FHIR Bundle transactions.

Implementation Details (Backend Support)

1. Bundle FHIR Client

File: internal/app/services/fhir_spark/bundle/bundle_fhir_impl.go

type BundleFhirClient interface {
    PostTransactionBundle(ctx context.Context, bundle map[string]any) (*fhir_dto.FHIRBundle, error)
}

2. Middleware Auth – Bundle Validation

File: internal/app/delivery/http/middlewares/auth.go

Auth uses scanBundle() to validate every entry in the Bundle (resourceType, RBAC, resource ownership, structure). If any entry fails, the whole request is rejected.

3. Middleware Bridge – Bundle Proxy

File: internal/app/delivery/http/middlewares/proxy.go

Bridge forwards the request (including Bundle) to the FHIR server with Content-Type: application/fhir+json.

4. Router

File: internal/app/delivery/http/routers/router.go

router.With(middlewares.Auth).
    Mount("/fhir", middlewares.Bridge(internalConfig.FHIR.BaseUrl))

FHIR Bundle Transaction Shape

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "request": {
        "method": "PUT",
        "url": "PractitionerRole/{id1}"
      },
      "resource": {
        "resourceType": "PractitionerRole",
        "id": "{id1}",
        "availableTime": [
          {
            "daysOfWeek": ["mon", "tue"],
            "availableStartTime": "09:00:00",
            "availableEndTime": "17:00:00"
          }
        ]
      }
    }
  ]
}

Request and Validation Flow

  1. Request: Frontend → POST /fhir (Bundle) → Auth → Bridge → FHIR server
  2. Validation: Auth detects Bundle, runs scanBundle() on each entry (RBAC + ownership). If all pass → Bridge forwards; otherwise → error.
  3. Transaction: FHIR server processes the Bundle atomically (all succeed or all roll back); response is returned to the client.

Benefits

  • Atomicity: All availability updates succeed or fail together; no partial updates.
  • Consistency: No inconsistent state on partial failure; automatic rollback.
  • Performance: One network round-trip instead of many sequential requests.
  • Reliability: Clear success/failure and simpler error handling.

Flow Comparison

Before (sequential)
Update #1 ✅ → #2 ✅ → #3 ❌ → inconsistent state; user must fix manually.

After (Bundle)
Single Bundle → all succeed or all fail → consistent state; user can retry the whole operation.

Error Handling

If any entry fails validation or the FHIR server rejects the transaction, the backend can return an OperationOutcome; the FHIR server rolls back the whole transaction. The frontend gets a single error and can retry the full batch.

Files Involved

  • internal/app/services/fhir_spark/bundle/bundle_fhir_impl.go – Bundle client
  • internal/app/delivery/http/middlewares/auth.goscanBundle()
  • internal/app/delivery/http/middlewares/proxy.go – Bridge
  • internal/app/delivery/http/routers/router.go/fhir route
  • internal/pkg/fhir_dto/bundle.go – Bundle DTO

Frontend implementation: konsulin-app (schedule API and practitioner-availability-editor).

Testing

  • Unit: scanBundle() with different Bundle structures and error cases.
  • Integration: Successful Bundle transaction; one invalid entry; verify atomicity.
  • E2E: Availability editor with multiple locations; single Bundle request; error recovery.

References

Summary by CodeRabbit

  • New Features

    • Sessions and access tokens now include a user FHIR resource ID when available; token issuance performs a read-only lookup to attach the best-matching resource (Practitioner, Patient, or Person).
    • Improved magic-link/email and SMS delivery flows with additional validation and timeout handling.
  • Refactor

    • Unified session initialization and context population for authenticated and anonymous requests; consolidated role initialization and context keys.

@qodo-code-review
Copy link
Contributor

PR Type

Enhancement


Description

  • Add FHIR resource ID enrichment to SuperTokens access token payload

  • Extract and store FHIR resource ID in request context via middleware

  • Implement role-based FHIR resource ID derivation with priority logic

  • Enable authorization checks without additional database queries


File Walkthrough

Relevant files
Enhancement
session.go
Extract and store FHIR resource ID in request context       

internal/app/delivery/http/middlewares/session.go

  • Added keyFHIRResourceId and supertokenAccessTokenPayloadFhirResourceId
    constants
  • Extract fhirResourceId from access token payload in SessionOptional
    middleware
  • Store FHIR resource ID in request context using both deprecated and
    typed context keys
  • Fixed indentation formatting in the middleware function
+49/-37 
auth_supertoken_impl.go
Implement FHIR resource ID derivation and token enrichment

internal/app/services/core/auth/auth_supertoken_impl.go

  • Added supertokenAccessTokenPayloadFhirResourceId constant for token
    payload key
  • Implemented getFhirResourceIdForUser() helper function with role-based
    priority logic
  • Integrated FHIR resource ID generation into CreateNewSession override
  • Populate access token payload with FHIR resource ID for authenticated
    and guest users
  • Added error handling and logging for FHIR resource ID retrieval
+68/-2   
internal_app.go
Add FHIR resource ID context key constant                               

internal/pkg/constvars/internal_app.go

  • Added CONTEXT_FHIR_RESOURCE_ID typed context key constant
  • Maintains consistency with other context key definitions
+1/-0     

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Warning

Rate limit exceeded

@gilanglahat22 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 39 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds lookup and propagation of a user's FHIR resource ID: new context key, middleware extracts fhirResourceId from access token into request context, auth service resolves and embeds fhirResourceId into SuperTokens access token payload, and user usecase exposes a read-only FHIR ID lookup.

Changes

Cohort / File(s) Summary
Context Key Declaration
internal/pkg/constvars/internal_app.go
Adds CONTEXT_FHIR_RESOURCE_ID (ContextKey) constant.
Session Middleware
internal/app/delivery/http/middlewares/session.go
Introduces ContextKey type, converts several context keys to typed constants, extracts fhirResourceId from access token payload and populates context for both authenticated and anonymous paths; unifies anonymous session handling.
Auth Service (Supertoken overrides & session)
internal/app/services/core/auth/auth_supertoken_impl.go
Adds supertokenAccessTokenPayloadFhirResourceId constant; adds getFhirResourceIdForUser and multiple helper wrappers to override Supertoken hooks (CreateCode, ConsumeCode, CreateNewSession, delivery/validation hooks); attaches resolved fhirResourceId to access token payload and ensures role initialization.
User contracts
internal/app/contracts/user.go
Adds LookupUserFHIRResourceIDsInput type and extends UserUsecase with LookupUserFHIRResourceIDs(...) for read-only FHIR ID lookups.
User usecase implementation
internal/app/services/core/users/user_usecase_impl.go
Implements LookupUserFHIRResourceIDs: validates input, searches Practitioner/Patient/Person by SuperTokenUserID, returns any found IDs in InitializeNewUserFHIRResourcesOutput, logs lookup errors without creating resources.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main feature: adding FHIR resource ID handling to both session middleware and authentication flows, which aligns with the substantial changes across session.go and auth_supertoken_impl.go.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 Security concerns

Token payload trust:
Middleware trusts access token claims (roles and fhirResourceId) as-is. Ensure SuperTokens verification guarantees integrity and the keys used are namespaced to avoid collisions. Also, be cautious logging fhirResourceId and user_ids; while not secrets, they are identifiers—confirm logs comply with privacy policies.

⚡ Recommended focus areas for review

Performance

getFhirResourceIdForUser initializes or fetches FHIR resources on every session creation and uses a 10s deadline. This can add latency to login/refresh and put load on the FHIR/DB backends. Consider caching the computed resource ID or moving this to a lazy fetch outside the hot path, and avoid creating resources unnecessarily.

// getFhirResourceIdForUser determines the FHIR resource ID based on user's roles and FHIR resource IDs
// Priority: Practitioner > Patient > Person
func (uc *authUsecase) getFhirResourceIdForUser(ctx context.Context, userID string, roles []string) (string, error) {
	// Initialize FHIR resources input
	initFHIRResourcesInput := &contracts.InitializeNewUserFHIRResourcesInput{
		SuperTokenUserID: userID,
	}
	initFHIRResourcesInput.ToogleByRoles(roles)

	// Get or create FHIR resources
	initializeResourceCtx, initializeResourceCtxCancel := context.WithDeadline(ctx, time.Now().Add(10*time.Second))
	defer initializeResourceCtxCancel()

	initializedResources, err := uc.UserUsecase.InitializeNewUserFHIRResources(initializeResourceCtx, initFHIRResourcesInput)
	if err != nil {
		uc.Log.Error("authUsecase.getFhirResourceIdForUser error initializing FHIR resources",
			zap.Error(err),
		)
		return "", err
	}

	// Determine FHIR resource ID based on role priority:
	// 1. If Practitioner role exists → Practitioner/{ID}
	// 2. Else if Patient role exists → Patient/{ID}
	// 3. Otherwise → Person/{ID}
	for _, role := range roles {
		if role == constvars.KonsulinRolePractitioner && initializedResources.PractitionerID != "" {
			return fmt.Sprintf("Practitioner/%s", initializedResources.PractitionerID), nil
		}
	}

	for _, role := range roles {
		if role == constvars.KonsulinRolePatient && initializedResources.PatientID != "" {
			return fmt.Sprintf("Patient/%s", initializedResources.PatientID), nil
		}
	}

	if initializedResources.PersonID != "" {
		return fmt.Sprintf("Person/%s", initializedResources.PersonID), nil
	}

	return "", errors.New("no FHIR resource ID found for user")
}
Context Usage

CreateNewSession calls getFhirResourceIdForUser with context.Background(), losing request cancellation/timeouts and trace info. Pass through the provided userContext or a request-scoped context to ensure proper cancelation and observability.

// Get FHIR resource ID for the user based on their roles
ctx := context.Background()
fhirResourceId, fhirErr := uc.getFhirResourceIdForUser(ctx, userID, userRoles)
if fhirErr != nil {
	uc.Log.Error("authUsecase.CreateNewSession error getting FHIR resource ID",
		zap.String("user_id", userID),
		zap.Error(fhirErr),
	)
	accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = ""
} else {
	accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = fhirResourceId
	uc.Log.Info("authUsecase.CreateNewSession added FHIR resource ID to access token",
		zap.String("user_id", userID),
		zap.String("fhir_resource_id", fhirResourceId),
	)
}
Clarity

Two context keys carry roles: untyped keyRoles and typed CONTEXT_FHIR_ROLE. This duplication can cause confusion and drift. Prefer a single typed key and plan removal of the deprecated untyped keys promptly.

ctx := context.WithValue(r.Context(), keyRoles, roles)
ctx = context.WithValue(ctx, keyUID, uid)
ctx = context.WithValue(ctx, keyFHIRResourceId, fhirResourceId)

// new keys for context will be used for now and one and this
// will deprecate the use of untyped string in context keys
ctx = context.WithValue(ctx, constvars.CONTEXT_FHIR_ROLE, roles)
ctx = context.WithValue(ctx, constvars.CONTEXT_UID, uid)
ctx = context.WithValue(ctx, constvars.CONTEXT_FHIR_RESOURCE_ID, fhirResourceId)

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Feb 4, 2026

PR Code Suggestions ✨

No code suggestions found for the PR.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View issue and 5 additional flags in Devin Review.

Open in Devin Review

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/app/delivery/http/middlewares/session.go (1)

130-143: ⚠️ Potential issue | 🟡 Minor

Inconsistent context key population in EnsureAnonymousSession.

This middleware sets keyRoles and keyUID but doesn't set the new keyFHIRResourceId or the typed constvars.CONTEXT_* keys that SessionOptional now provides. Downstream handlers expecting CONTEXT_FHIR_RESOURCE_ID may encounter missing context values.

Proposed fix
 		if sess == nil {
 
 			ctx := context.WithValue(r.Context(), keyRoles, []string{constvars.KonsulinRoleGuest})
 			ctx = context.WithValue(ctx, keyUID, "anonymous")
+			ctx = context.WithValue(ctx, keyFHIRResourceId, "")
+
+			ctx = context.WithValue(ctx, constvars.CONTEXT_FHIR_ROLE, []string{constvars.KonsulinRoleGuest})
+			ctx = context.WithValue(ctx, constvars.CONTEXT_UID, "anonymous")
+			ctx = context.WithValue(ctx, constvars.CONTEXT_FHIR_RESOURCE_ID, "")
 
 			m.Log.Info("Ensuring anonymous session for request",
🧹 Nitpick comments (1)
internal/app/services/core/auth/auth_supertoken_impl.go (1)

60-70: Optional: Single-pass role check.

Two loops iterate the roles array. Could consolidate into one pass, though current approach is readable.

Single-pass alternative
-	for _, role := range roles {
-		if role == constvars.KonsulinRolePractitioner && initializedResources.PractitionerID != "" {
-			return fmt.Sprintf("Practitioner/%s", initializedResources.PractitionerID), nil
-		}
-	}
-
-	for _, role := range roles {
-		if role == constvars.KonsulinRolePatient && initializedResources.PatientID != "" {
-			return fmt.Sprintf("Patient/%s", initializedResources.PatientID), nil
-		}
-	}
+	hasPractitioner, hasPatient := false, false
+	for _, role := range roles {
+		if role == constvars.KonsulinRolePractitioner {
+			hasPractitioner = true
+		} else if role == constvars.KonsulinRolePatient {
+			hasPatient = true
+		}
+	}
+
+	if hasPractitioner && initializedResources.PractitionerID != "" {
+		return fmt.Sprintf("Practitioner/%s", initializedResources.PractitionerID), nil
+	}
+	if hasPatient && initializedResources.PatientID != "" {
+		return fmt.Sprintf("Patient/%s", initializedResources.PatientID), nil
+	}

initializeResourceCtx, initializeResourceCtxCancel := context.WithDeadline(ctx, time.Now().Add(10*time.Second))
defer initializeResourceCtxCancel()

initializedResources, err := uc.UserUsecase.InitializeNewUserFHIRResources(initializeResourceCtx, initFHIRResourcesInput)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mau tanya mas @gilanglahat22 , Apakah ada alasan khusus kenapa memilih reuse function InitializeNewUserFHIRResources ketimbang membuat function baru yang khusus untuk melakukan query FHIR resources yang dimiliki oleh requester?

Kalau dilihat secara internal dari InitializeNewUserFHIRResources, focus function tersebut kan write operation, sedangkan yang dibutuhkan di bagian ini seharusnya read operations. Kemudian juga secara penamaan, function InitializeNewUserFHIRResources sudah jelas tujuannya tidak diperuntukan untuk mengambil FHIR resources berdasarkan SuperTokenUserID


// getFhirResourceIdForUser determines the FHIR resource ID based on user's roles and FHIR resource IDs
// Priority: Practitioner > Patient > Person
func (uc *authUsecase) getFhirResourceIdForUser(ctx context.Context, userID string, roles []string) (string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment is not a change request to the code, just for discussion

mas @lamurian , aku recheck current issue yang akan di solve oleh PR ini dan secara teknis ini adalah function utama yang akan mengambil FHIR resources yang dimiliki oleh user. Secara teknis juga implementasinya sudah sesuai dengan issuenya.

Namun yang mau saya tanyakan adalah ini jadinya tetap tidak menghandle case dimana ketika user role adalah Practitioner, dia kan akan memiliki setidaknya 2 FHIR resources, Patient dan juga Practitioner. Untuk case demikian, apakah jadinya kita tidak akan embed semua resources milik user?

Copy link
Member

@lamurian lamurian Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Mas @luckyAkbar, thank you for bringing this topic up. I believe it's best to best to embed all user resource IDs in the metadata. I expect the metadata for roles and user ID to be:

{
  "roles": [
    {
      "name": "Patient",
      "id": "PatientID"
    },
    {
      "name": "Practitioner",
      "id": "PractitionerID"
    },
    {
      "name": "Clinic Admin",
      "id": "ClinicAdminID"
    },
    {
      "name": "Researcher",
      "id": "ResearcherID"
    }
  ]
}

When accessing resources related to each roles:

  • Patient $\to$ Access resource Patient/PatientID
  • Practitioner $\to$ Access resource Practitioner/PractitionerID
  • Clinic Admin $\to$ Access resource Person/ClinicAdminID
  • Researcher $\to$ Access resource Person/ResearcherID

Mas @gilanglahat22 please note that for roles Clinic Admin and Researcher, the Person resource is used because there's no dedicated FHIR resource for these roles..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment is not a change request to the code, just for discussion

mas @lamurian , aku recheck current issue yang akan di solve oleh PR ini dan secara teknis ini adalah function utama yang akan mengambil FHIR resources yang dimiliki oleh user. Secara teknis juga implementasinya sudah sesuai dengan issuenya.

Namun yang mau saya tanyakan adalah ini jadinya tetap tidak menghandle case dimana ketika user role adalah Practitioner, dia kan akan memiliki setidaknya 2 FHIR resources, Patient dan juga Practitioner. Untuk case demikian, apakah jadinya kita tidak akan embed semua resources milik user?

Setuju mas @luckyAkbar, memang harusnya tidak reuse InitializeNewUserFHIRResources karena fokusnya write operation. Sudah saya buatkan function baru khusus untuk read operation:

func (uc *userUsecase) LookupUserFHIRResourceIDs(ctx context.Context, input *LookupUserFHIRResourceIDsInput) (*InitializeNewUserFHIRResourcesOutput, error)

Function ini hanya query existing FHIR resources berdasarkan SuperTokenUserID, tanpa create resource baru. Sudah menggunakan FindPractitionerByIdentifier, FindPatientByIdentifier, dan PersonFhirClient.Search.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Mas @luckyAkbar, thank you for bringing this topic up. I believe it's best to best to embed all user resource IDs in the metadata. I expect the metadata for roles and user ID to be:

{
  "roles": [
    {
      "name": "Patient",
      "id": "PatientID"
    },
    {
      "name": "Practitioner",
      "id": "PractitionerID"
    },
    {
      "name": "Clinic Admin",
      "id": "ClinicAdminID"
    },
    {
      "name": "Researcher",
      "id": "ResearcherID"
    }
  ]
}

When accessing resources related to each roles:

  • Patient

    Access resource Patient/PatientID
  • Practitioner

    Access resource Practitioner/PractitionerID
  • Clinic Admin

    Access resource Person/ClinicAdminID
  • Researcher

    Access resource Person/ResearcherID

Mas @gilanglahat22 please note that for roles Clinic Admin and Researcher, the Person resource is used because there's no dedicated FHIR resource for these roles..

Terima kasih discussionnya mas @luckyAkbar dan mas @lamurian. Untuk sekarang implementasinya masih menggunakan priority-based approach (Practitioner > Patient > Person) untuk menentukan fhirResourceId yang di-embed di access token.

Untuk case user dengan multiple roles (e.g., Practitioner + Patient), saat ini kita hanya embed satu primary resource ID. Jika ke depan ada kebutuhan untuk embed semua resource IDs seperti yang mas @lamurian sarankan:

{
  "roles": [
    {"name": "Patient", "id": "PatientID"},
    {"name": "Practitioner", "id": "PractitionerID"}
  ]
}

Bisa di-refactor lagi. Untuk saat ini PR ini fokus fix bug validation error dulu. Apakah perlu dibuatkan issue terpisah untuk enhancement ini?

@deepsource-io
Copy link

deepsource-io bot commented Feb 7, 2026

Here's the code health analysis summary for commits 9bb4e5f..0c9efb0. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource Go LogoGo✅ Success
🎯 8 occurences resolved
View Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

…ess token enrichment based on user roles and document the solution.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@internal/app/services/core/auth/auth_supertoken_impl.go`:
- Around line 479-503: The code in authUsecase.CreateNewSession uses two
separate checks for fhirErr (if fhirErr != nil and if fhirErr == nil) after
calling uc.getFhirResourceIdForUser; simplify to a single if/else: call
uc.getFhirResourceIdForUser, then if fhirErr != nil log the error and set
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = "" in the if
branch, else set accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId]
= fhirResourceId and log the success; remove the redundant second if and keep
the same logging/messages and keys (accessTokenPayload,
supertokenAccessTokenPayloadFhirResourceId,
supertokenAccessTokenPayloadRolesKey).

In `@internal/app/services/core/users/user_usecase_impl.go`:
- Around line 384-438: LookupUserFHIRResourceIDs currently swallows all errors
from uc.PractitionerFhirClient.FindPractitionerByIdentifier,
uc.PatientFhirClient.FindPatientByIdentifier, and uc.PersonFhirClient.Search and
always returns (output, nil), making callers' err checks (e.g.,
getFhirResourceIdForUser) useless; update LookupUserFHIRResourceIDs to return a
meaningful error when appropriate — e.g., if all three lookups failed (no IDs
found and each call returned a non-nil error) return a consolidated error (or
wrap the first error) so callers can handle failure, otherwise keep returning
(output, nil) when at least one lookup succeeded; reference the function name
LookupUserFHIRResourceIDs and the FHIR client calls
(FindPractitionerByIdentifier, FindPatientByIdentifier, PersonFhirClient.Search)
to locate where to implement this logic.

Comment on lines 479 to 503

// Get FHIR resource ID for the user based on their roles
ctx := context.Background()
fhirResourceId, fhirErr := uc.getFhirResourceIdForUser(ctx, userID, userRoles)
if fhirErr != nil {
uc.Log.Error("authUsecase.CreateNewSession error getting FHIR resource ID",
zap.String("user_id", userID),
zap.Error(fhirErr),
)
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = ""
}

if fhirErr == nil {
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = fhirResourceId
uc.Log.Info("authUsecase.CreateNewSession added FHIR resource ID to access token",
zap.String("user_id", userID),
zap.String("fhir_resource_id", fhirResourceId),
)
}
} else {
accessTokenPayload[supertokenAccessTokenPayloadRolesKey] = map[string]interface{}{
supertokenAccessTokenPayloadRolesValueKey: []interface{}{constvars.KonsulinRoleGuest},
}
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = ""
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Simplify the fhirErr check — use else instead of two separate if blocks.

Lines 483-497 check fhirErr != nil and then separately check fhirErr == nil. Use a single if/else.

Suggested diff
 								fhirResourceId, fhirErr := uc.getFhirResourceIdForUser(ctx, userID, userRoles)
 								if fhirErr != nil {
 									uc.Log.Error("authUsecase.CreateNewSession error getting FHIR resource ID",
 										zap.String("user_id", userID),
 										zap.Error(fhirErr),
 									)
 									accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = ""
-								}
-
-								if fhirErr == nil {
+								} else {
 									accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = fhirResourceId
 									uc.Log.Info("authUsecase.CreateNewSession added FHIR resource ID to access token",
 										zap.String("user_id", userID),
 										zap.String("fhir_resource_id", fhirResourceId),
 									)
 								}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Get FHIR resource ID for the user based on their roles
ctx := context.Background()
fhirResourceId, fhirErr := uc.getFhirResourceIdForUser(ctx, userID, userRoles)
if fhirErr != nil {
uc.Log.Error("authUsecase.CreateNewSession error getting FHIR resource ID",
zap.String("user_id", userID),
zap.Error(fhirErr),
)
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = ""
}
if fhirErr == nil {
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = fhirResourceId
uc.Log.Info("authUsecase.CreateNewSession added FHIR resource ID to access token",
zap.String("user_id", userID),
zap.String("fhir_resource_id", fhirResourceId),
)
}
} else {
accessTokenPayload[supertokenAccessTokenPayloadRolesKey] = map[string]interface{}{
supertokenAccessTokenPayloadRolesValueKey: []interface{}{constvars.KonsulinRoleGuest},
}
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = ""
}
// Get FHIR resource ID for the user based on their roles
ctx := context.Background()
fhirResourceId, fhirErr := uc.getFhirResourceIdForUser(ctx, userID, userRoles)
if fhirErr != nil {
uc.Log.Error("authUsecase.CreateNewSession error getting FHIR resource ID",
zap.String("user_id", userID),
zap.Error(fhirErr),
)
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = ""
} else {
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = fhirResourceId
uc.Log.Info("authUsecase.CreateNewSession added FHIR resource ID to access token",
zap.String("user_id", userID),
zap.String("fhir_resource_id", fhirResourceId),
)
}
} else {
accessTokenPayload[supertokenAccessTokenPayloadRolesKey] = map[string]interface{}{
supertokenAccessTokenPayloadRolesValueKey: []interface{}{constvars.KonsulinRoleGuest},
}
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = ""
}
🤖 Prompt for AI Agents
In `@internal/app/services/core/auth/auth_supertoken_impl.go` around lines 479 -
503, The code in authUsecase.CreateNewSession uses two separate checks for
fhirErr (if fhirErr != nil and if fhirErr == nil) after calling
uc.getFhirResourceIdForUser; simplify to a single if/else: call
uc.getFhirResourceIdForUser, then if fhirErr != nil log the error and set
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = "" in the if
branch, else set accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId]
= fhirResourceId and log the success; remove the redundant second if and keep
the same logging/messages and keys (accessTokenPayload,
supertokenAccessTokenPayloadFhirResourceId,
supertokenAccessTokenPayloadRolesKey).

@pullflow-com
Copy link

pullflow-com bot commented Feb 8, 2026

From Gilang Ramadhan ‣ udah aku coba fix ya Mas @lamurian @luckyAkbar monggo minta tolong dicek 👍

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 12 additional findings in Devin Review.

Open in Devin Review

… improve SuperTokens role creation error handling with nil checks.
… logic into dedicated methods and create PR reply documentation.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/app/delivery/http/middlewares/session.go (1)

120-148: ⚠️ Potential issue | 🟠 Major

EnsureAnonymousSession must set constvars.CONTEXT_* keys to match SessionOptional.

This middleware only sets the deprecated local keys (keyRoles, keyUID) but omits constvars.CONTEXT_UID and constvars.CONTEXT_FHIR_RESOURCE_ID. Multiple downstream handlers read from these constvars keys and will receive zero values when this middleware is used, causing bugs. Add:

ctx = context.WithValue(ctx, constvars.CONTEXT_FHIR_ROLE, []string{constvars.KonsulinRoleGuest})
ctx = context.WithValue(ctx, constvars.CONTEXT_UID, "anonymous")
ctx = context.WithValue(ctx, constvars.CONTEXT_FHIR_RESOURCE_ID, "")
🤖 Fix all issues with AI agents
In `@internal/app/delivery/http/middlewares/session.go`:
- Around line 85-89: The comment above the context.WithValue calls is garbled;
replace it with a clear sentence such as: "Typed context keys
(constvars.ContextKey) — these will replace the deprecated local ContextKey keys
above." Update the comment located near the context.WithValue(...) lines that
set constvars.CONTEXT_FHIR_ROLE, constvars.CONTEXT_UID, and
constvars.CONTEXT_FHIR_RESOURCE_ID so it clearly states that typed constvars
keys are used and will deprecate untyped string context keys.

In `@internal/app/services/core/auth/auth_supertoken_impl.go`:
- Around line 167-177: Remove the five calls to the undefined package-level
function ensureRoleExists() (the bare calls at the start of the list) and keep
only the method calls on the use case receiver uc (uc.ensureRoleExists(...));
specifically delete the calls to
ensureRoleExists(constvars.KonsulinRolePatient),
ensureRoleExists(constvars.KonsulinRoleGuest),
ensureRoleExists(constvars.KonsulinRoleClinicAdmin),
ensureRoleExists(constvars.KonsulinRolePractitioner), and
ensureRoleExists(constvars.KonsulinRoleResearcher) so only
uc.ensureRoleExists(...) and
uc.ensureRoleExists(constvars.KonsulinRoleSuperadmin) remain, relying on the
defined method uc.ensureRoleExists.
🧹 Nitpick comments (1)
internal/app/services/core/auth/auth_supertoken_impl.go (1)

244-263: Default userRoles always includes Patient, then appends SuperTokens roles — potential duplicate.

userRoles starts as [Patient] (line 244), then line 262 appends the user's actual roles. If the user already has the Patient role in SuperTokens, the slice becomes [Patient, Patient, ...]. This likely doesn't cause a bug since ToogleByRoles and getFhirResourceIdForUser just scan for membership, but it's worth cleaning up.

Proposed fix
-			if userRolesResp.OK != nil {
-				// override the default roles with the user roles from supertokens
-				userRoles = append(userRoles, userRolesResp.OK.Roles...)
-			}
+			if userRolesResp.OK != nil && len(userRolesResp.OK.Roles) > 0 {
+				// override the default roles with the user roles from supertokens
+				userRoles = userRolesResp.OK.Roles
+			}

Comment on lines +167 to +177
ensureRoleExists(constvars.KonsulinRolePatient)
ensureRoleExists(constvars.KonsulinRoleGuest)
ensureRoleExists(constvars.KonsulinRoleClinicAdmin)
ensureRoleExists(constvars.KonsulinRolePractitioner)
ensureRoleExists(constvars.KonsulinRoleResearcher)
uc.ensureRoleExists(constvars.KonsulinRolePatient)
uc.ensureRoleExists(constvars.KonsulinRoleGuest)
uc.ensureRoleExists(constvars.KonsulinRoleClinicAdmin)
uc.ensureRoleExists(constvars.KonsulinRolePractitioner)
uc.ensureRoleExists(constvars.KonsulinRoleResearcher)
uc.ensureRoleExists(constvars.KonsulinRoleSuperadmin)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a package-level ensureRoleExists function (non-method)
echo "=== All ensureRoleExists declarations ==="
rg -n 'func\s+ensureRoleExists' --type=go
echo ""
echo "=== Method receiver version ==="
rg -n 'func\s+\(.*\)\s+ensureRoleExists' --type=go

Repository: konsulin-care/konsulin-api

Length of output: 259


🏁 Script executed:

sed -n '160,185p' internal/app/services/core/auth/auth_supertoken_impl.go

Repository: konsulin-care/konsulin-api

Length of output: 980


🏁 Script executed:

# Check for any imports or aliases of ensureRoleExists
rg -n 'ensureRoleExists' internal/app/services/core/auth/auth_supertoken_impl.go | head -20

Repository: konsulin-care/konsulin-api

Length of output: 752


🏁 Script executed:

# Check if ensureRoleExists could be defined elsewhere in the file or imported
echo "=== Check file for all 'ensureRoleExists' mentions ==="
rg -c 'ensureRoleExists' internal/app/services/core/auth/auth_supertoken_impl.go

echo ""
echo "=== Check package-level functions in the file ==="
rg -n '^func [a-zA-Z]' internal/app/services/core/auth/auth_supertoken_impl.go | head -10

echo ""
echo "=== Verify if code compiles ==="
cd internal/app/services/core/auth && go build -o /tmp/test 2>&1 | grep -i "ensureRoleExists\|undefined" || echo "No compilation errors related to ensureRoleExists"

Repository: konsulin-care/konsulin-api

Length of output: 516


Remove undefined ensureRoleExists() calls — code does not compile.

Lines 167–171 call bare ensureRoleExists() without a receiver, but no package-level function with that name exists. Only the method uc.ensureRoleExists() (defined at line 183) is available. These calls cause 5 compilation errors. Remove lines 167–171:

Proposed fix
-	ensureRoleExists(constvars.KonsulinRolePatient)
-	ensureRoleExists(constvars.KonsulinRoleGuest)
-	ensureRoleExists(constvars.KonsulinRoleClinicAdmin)
-	ensureRoleExists(constvars.KonsulinRolePractitioner)
-	ensureRoleExists(constvars.KonsulinRoleResearcher)
 	uc.ensureRoleExists(constvars.KonsulinRolePatient)
 	uc.ensureRoleExists(constvars.KonsulinRoleGuest)
 	uc.ensureRoleExists(constvars.KonsulinRoleClinicAdmin)
 	uc.ensureRoleExists(constvars.KonsulinRolePractitioner)
 	uc.ensureRoleExists(constvars.KonsulinRoleResearcher)
 	uc.ensureRoleExists(constvars.KonsulinRoleSuperadmin)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ensureRoleExists(constvars.KonsulinRolePatient)
ensureRoleExists(constvars.KonsulinRoleGuest)
ensureRoleExists(constvars.KonsulinRoleClinicAdmin)
ensureRoleExists(constvars.KonsulinRolePractitioner)
ensureRoleExists(constvars.KonsulinRoleResearcher)
uc.ensureRoleExists(constvars.KonsulinRolePatient)
uc.ensureRoleExists(constvars.KonsulinRoleGuest)
uc.ensureRoleExists(constvars.KonsulinRoleClinicAdmin)
uc.ensureRoleExists(constvars.KonsulinRolePractitioner)
uc.ensureRoleExists(constvars.KonsulinRoleResearcher)
uc.ensureRoleExists(constvars.KonsulinRoleSuperadmin)
uc.ensureRoleExists(constvars.KonsulinRolePatient)
uc.ensureRoleExists(constvars.KonsulinRoleGuest)
uc.ensureRoleExists(constvars.KonsulinRoleClinicAdmin)
uc.ensureRoleExists(constvars.KonsulinRolePractitioner)
uc.ensureRoleExists(constvars.KonsulinRoleResearcher)
uc.ensureRoleExists(constvars.KonsulinRoleSuperadmin)
🤖 Prompt for AI Agents
In `@internal/app/services/core/auth/auth_supertoken_impl.go` around lines 167 -
177, Remove the five calls to the undefined package-level function
ensureRoleExists() (the bare calls at the start of the list) and keep only the
method calls on the use case receiver uc (uc.ensureRoleExists(...));
specifically delete the calls to
ensureRoleExists(constvars.KonsulinRolePatient),
ensureRoleExists(constvars.KonsulinRoleGuest),
ensureRoleExists(constvars.KonsulinRoleClinicAdmin),
ensureRoleExists(constvars.KonsulinRolePractitioner), and
ensureRoleExists(constvars.KonsulinRoleResearcher) so only
uc.ensureRoleExists(...) and
uc.ensureRoleExists(constvars.KonsulinRoleSuperadmin) remain, relying on the
defined method uc.ensureRoleExists.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the redundant and undefined bare function calls to ensureRoleExists(...) in internal/app/services/core/auth/auth_supertoken_impl.go (lines 167-171). The code now correctly uses only the method calls uc.ensureRoleExists(...).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for addressing this concern Mas @gilanglahat22. But there seems to be other underlying problems that cause compilation failure, as detected in our PR screening.

…nsureRoleExists` method calls to use the receiver.
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 12 additional findings in Devin Review.

Open in Devin Review

… across various modules and refactor auth supertoken functions.
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 8, 2026

Comment on lines +562 to +568

if fhirErr == nil {
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = fhirResourceId
uc.Log.Info("authUsecase.CreateNewSession added FHIR resource ID to access token",
zap.String("user_id", userID),
zap.String("fhir_resource_id", fhirResourceId),
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

untuk block if fhirErr == nil ini juga menurut saya tidak diperlukan, mas, karena sudah pasti value fhirErr adalah nil, kan.

Lebih baik if checking nya itu mengecek fhirResourceId, apakah dia nil atau apakah mengandung value tertentu yang dianggap valid untuk digunakan di accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId]

jadi semisal contohnya

if fhirResourceId != "" {
    // assignments here
}

Comment on lines +533 to +538

if userID == "" {
accessTokenPayload[supertokenAccessTokenPayloadRolesKey] = map[string]interface{}{
supertokenAccessTokenPayloadRolesValueKey: []interface{}{constvars.KonsulinRoleGuest},
}
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kemudian untuk bagian ini, sepemahaman saya adalah ketika userID dari supertokens tidak ditemukan, maka payload di accessTokenPayload hanya akan diinisiasi sbg role Guest dan juga resource id nya di set ke empty string.

Supaya untuk mempermudah kodenya di baca, kita bisa pakai gaya penulisan code negative space programming. Karena case userID == "" ini dianggap sebagai error / default behaviour, kita bisa langsung melakukan menambahkan return line (Seperti yang ada di baris 578) untuk langsung skip semua kode yang ada di bawahnya.

Setelah itu, else block yang menempel (line 539) bisa langsung kita hapus karena ketika case userID == "" kode akan langsung exit di block tersebut. Akibatnya kode yang ditulis itu lebih rata ke kiri ketimbang berada di dalam nested if yang dalam.

Hal yang sama juga saya sarankan untuk menghilangkan block else di baris ke 570 - 576 dengan cara lakukan error check yang lebih eksplisit dari hasil operasi function line 540. Gambaran saya seperti ini

rolesResp, err := userroles.GetRolesForUser(tenantId, userID)
if err != nil || roleresp.OK == nil  {
// bisa di log dulu kenapa supertokens error
accessTokenPayload[supertokenAccessTokenPayloadRolesKey] = map[string]interface{}{
  supertokenAccessTokenPayloadRolesValueKey: []interface{}{constvars.KonsulinRoleGuest},
  }
accessTokenPayload[supertokenAccessTokenPayloadFhirResourceId] = ""

return originalCreateSession....
}

// rest of the code here (line 542 - last return line

Menurut saya ini gak mengubah flow execution yang sudah di buat, melainkan hanya melakukan early exit ketika kode mengalami kondisi yang tidak diinginkan / tidak ideal dan sekaligus mengurangi nested kode, mas @gilanglahat22 . Please LMK your response ya mas

@pullflow-com
Copy link

pullflow-com bot commented Feb 17, 2026

From Gilang Ramadhan ‣ Sure, I'll fix it right now. Sorry, I'm just getting to it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants