1 line
8.9 KiB
JSON
1 line
8.9 KiB
JSON
{"path":"NexusReader.Application/Security/Authorization/ProUserHandler.cs","purpose":"Authorization handler that enforces 'Pro' user access by checking the current user's subscription and AI token usage against limits in the application's database.","classification":{"role":"handler","layer":"application","confidence":0.92,"evidence":["Class ProUserHandler extends AuthorizationHandler<T> (authorization handler)","Namespace NexusReader.Application.Security.Authorization","Uses IDbContextFactory<AppDbContext> to load user and subscription data"]},"className":"ProUserHandler","methods":[{"name":"ProUserHandler","line":15,"endLine":18,"signature":"(dbContextFactory: IDbContextFactory<AppDbContext>) -> void","purpose":"Constructor: receives and stores a DbContext factory for use by the handler.","calls":[],"actions":[{"id":"assign_17","kind":"mapping","label":"Store injected dependency","line":17,"detail":"Assigns _dbContextFactory = dbContextFactory","visibility":"detail-only","confidence":0.7}]},{"name":"HandleRequirementAsync","line":20,"endLine":53,"signature":"(context: AuthorizationHandlerContext, requirement: ProUserRequirement) -> Task","purpose":"Core authorization logic: loads the current user from the DB and grants the requirement when the user has unlimited tokens or remaining AI token quota.","calls":[],"actions":[{"id":"claim-read_24","kind":"mapping","label":"Read user id from ClaimsPrincipal","line":24,"detail":"var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier)","visibility":"detail-only","confidence":0.7},{"id":"handlerequirementasync_repository-read_24_0","kind":"repository-read","label":"Reads repository or persistence state","line":24,"detail":"var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);","visibility":"secondary-visible","confidence":0.86},{"id":"handlerequirementasync_branch_25_1","kind":"branch","label":"Evaluates branch condition","line":25,"detail":"if (string.IsNullOrEmpty(userId))","conditionSummary":"string.IsNullOrEmpty(userId)","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"guard-clause_25","kind":"guard-clause","label":"Reject when no user id available","line":25,"detail":"If no NameIdentifier claim, method returns early","conditionSummary":"string.IsNullOrEmpty(userId)","outcomeLabels":["return (no authorization attempt)"],"visibility":"detail-only","confidence":0.7},{"id":"handlerequirementasync_return_27_2","kind":"return","label":"Returns result","line":27,"detail":"return;","visibility":"detail-only","confidence":0.7},{"id":"db-context-create_30","kind":"mapping","label":"Create DbContext from factory (scoped local using)","line":30,"detail":"using var db = _dbContextFactory.CreateDbContext(); ensures disposal at end of block","visibility":"detail-only","confidence":0.7},{"id":"db-query_31","kind":"mapping","label":"Load user with subscription","line":31,"detail":"var user = await db.Users.Include(u => u.SubscriptionPlan).FirstOrDefaultAsync(u => u.Id == userId); — reads user and subscription from persistence","visibility":"detail-only","confidence":0.7},{"id":"handlerequirementasync_await_31_3","kind":"await","label":"Waits for async work","line":31,"detail":"var user = await db.Users","visibility":"secondary-visible","confidence":0.81},{"id":"handlerequirementasync_branch_35_4","kind":"branch","label":"Evaluates branch condition","line":35,"detail":"if (user == null)","conditionSummary":"user == null","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"null-check_35","kind":"mapping","label":"Return if user not found","line":35,"detail":"No requirement evaluation when the user record cannot be found","conditionSummary":"user == null","outcomeLabels":["return (unauthenticated/missing user)"],"visibility":"detail-only","confidence":0.7},{"id":"handlerequirementasync_return_37_5","kind":"return","label":"Returns result","line":37,"detail":"return;","visibility":"detail-only","confidence":0.7},{"id":"branch_40","kind":"branch","label":"Unlimited tokens rule","line":40,"detail":"If subscription marks unlimited tokens, the requirement is succeeded and method returns","conditionSummary":"user.SubscriptionPlan?.IsUnlimitedTokens == true","outcomeLabels":["context.Succeed(requirement)","return"],"visibility":"detail-only","confidence":0.7},{"id":"handlerequirementasync_branch_41_6","kind":"branch","label":"Evaluates branch condition","line":41,"detail":"if (user.SubscriptionPlan?.IsUnlimitedTokens == true)","conditionSummary":"user.SubscriptionPlan?.IsUnlimitedTokens == true","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"handlerequirementasync_repository-read_43_7","kind":"repository-read","label":"Reads repository or persistence state","line":43,"detail":"context.Succeed(requirement);","visibility":"secondary-visible","confidence":0.86},{"id":"handlerequirementasync_return_44_8","kind":"return","label":"Returns result","line":44,"detail":"return;","visibility":"detail-only","confidence":0.7},{"id":"branch_47","kind":"branch","label":"Token quota rule","line":47,"detail":"If user is still under their AI token limit, succeed requirement and return","conditionSummary":"user.AITokensUsed < user.AITokenLimit","outcomeLabels":["context.Succeed(requirement)","return"],"visibility":"detail-only","confidence":0.7},{"id":"handlerequirementasync_branch_48_9","kind":"branch","label":"Evaluates branch condition","line":48,"detail":"if (user.AITokensUsed < user.AITokenLimit)","conditionSummary":"user.AITokensUsed < user.AITokenLimit","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"handlerequirementasync_repository-read_50_10","kind":"repository-read","label":"Reads repository or persistence state","line":50,"detail":"context.Succeed(requirement);","visibility":"secondary-visible","confidence":0.86},{"id":"handlerequirementasync_return_51_11","kind":"return","label":"Returns result","line":51,"detail":"return;","visibility":"detail-only","confidence":0.7},{"id":"implicit-fallback_52","kind":"mapping","label":"Deny by omission","line":52,"detail":"If neither rule matched, method completes without calling context.Succeed — requirement is not granted","visibility":"detail-only","confidence":0.7}]}],"types":[],"serviceRegistrations":[],"startupActions":[],"dependencies":["NexusReader.Data.Persistence","NexusReader.Domain.Entities","Microsoft.EntityFrameworkCore"],"patterns":["Authorization Handler","Repository/Query (EF DbContext)"],"domainConcepts":["User","SubscriptionPlan","AITokens / AITokenLimit"],"keyDetails":"Enforces two rules: unlimited-subscription bypass and per-user AI token quota. Uses IDbContextFactory to create a short-lived AppDbContext and loads SubscriptionPlan via EF Include; succeeds the Authorization requirement when either rule passes.","orchestrationMethods":[{"name":"HandleRequirementAsync","line":20,"confidence":0.98,"reason":"Contains 9 architectural actions relevant to business execution.","actionKinds":["mapping","repository-read","branch","guard-clause","return","await"],"evidencePaths":["NexusReader.Application/Security/Authorization/ProUserHandler.cs"]}],"typedContracts":[],"persistenceInteractions":[{"methodName":"HandleRequirementAsync","line":24,"kind":"persistence-read","detail":"var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);","evidencePaths":["NexusReader.Application/Security/Authorization/ProUserHandler.cs"]},{"methodName":"HandleRequirementAsync","line":43,"kind":"persistence-read","detail":"context.Succeed(requirement);","evidencePaths":["NexusReader.Application/Security/Authorization/ProUserHandler.cs"]},{"methodName":"HandleRequirementAsync","line":50,"kind":"persistence-read","detail":"context.Succeed(requirement);","evidencePaths":["NexusReader.Application/Security/Authorization/ProUserHandler.cs"]}],"externalInteractions":[],"evidenceAnchors":[{"kind":"orchestration-method","label":"HandleRequirementAsync","line":20,"summary":"Contains 9 architectural actions relevant to business execution.","confidence":0.98,"evidencePaths":["NexusReader.Application/Security/Authorization/ProUserHandler.cs"]},{"kind":"persistence","label":"HandleRequirementAsync","line":24,"summary":"var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);","confidence":0.82,"evidencePaths":["NexusReader.Application/Security/Authorization/ProUserHandler.cs"]},{"kind":"persistence","label":"HandleRequirementAsync","line":43,"summary":"context.Succeed(requirement);","confidence":0.82,"evidencePaths":["NexusReader.Application/Security/Authorization/ProUserHandler.cs"]},{"kind":"persistence","label":"HandleRequirementAsync","line":50,"summary":"context.Succeed(requirement);","confidence":0.82,"evidencePaths":["NexusReader.Application/Security/Authorization/ProUserHandler.cs"]}],"cacheMetadata":{"schemaVersion":2,"analysisVersion":"2026-05-23.cache-v1","contentChecksum":"3c98cffb379922fbe5452ccd8cd40f5ea5ca0e1a7680583c2cf0d026b9cc17cd","sourceByteSize":1575,"analyzedAt":"2026-05-23T16:18:18.009Z","technology":"dotnet"}} |