{"record":{"id":"b4136253576a9fb5","repo":"Kareadita/Kavita","slug":"series-restricted-age-restriction-b41362","errorCode":"series-restricted-age-restriction","errorMessage":"series-restricted-age-restriction","messagePattern":"series-restricted-age-restriction","errorType":"exception","errorClass":"UnauthorizedAccessException","httpStatus":401,"severity":"error","filePath":"Kavita.Services/SeriesService.cs","lineNumber":539,"sourceCode":"    /// <param name=\"seriesId\"></param>\n    /// <param name=\"userId\"></param>\n    /// <param name=\"ct\"></param>\n    /// <returns></returns>\n    public async Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId, CancellationToken ct = default)\n    {\n        var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);\n        if (series == null) throw new KavitaException(await localizationService.TranslateAsync(userId, \"series-doesnt-exist\"));\n\n        var libraryIds = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct);\n        if (!libraryIds.Contains(series.LibraryId))\n            throw new UnauthorizedAccessException(\"user-no-access-library-from-series\");\n\n        var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct);\n        if (user!.AgeRestriction != AgeRating.NotApplicable)\n        {\n            var seriesMetadata = await unitOfWork.SeriesRepository.GetSeriesMetadataAsync(seriesId, ct);\n            if (seriesMetadata!.AgeRating > user.AgeRestriction)\n                throw new UnauthorizedAccessException(\"series-restricted-age-restriction\");\n        }\n\n\n        var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct);\n        var volumes = await unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId, ct: ct);\n        var namingContext = await LocalizedNamingContext.CreateAsync(namingService, localizationService, userId, libraryType);\n        var bookTreatment = libraryType is LibraryType.Book or LibraryType.LightNovel;\n\n        // For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number.\n        var processedVolumes = new List<VolumeDto>();\n        foreach (var volume in volumes)\n        {\n            if (volume.IsLooseLeaf() || volume.IsSpecial())\n            {\n                continue;\n            }\n\n            var formattedName = namingContext.FormatVolumeName(volume);","sourceCodeStart":521,"sourceCodeEnd":557,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/SeriesService.cs#L521-L557","documentation":"Thrown as UnauthorizedAccessException in GetSeriesDetail when the user has an AgeRestriction (not NotApplicable) and the series' metadata AgeRating exceeds it. Kavita enforces per-user age gates so restricted users cannot open series above their allowed rating.","triggerScenarios":"GetSeriesDetail for a series whose SeriesMetadata.AgeRating is greater than the requesting user's AgeRestriction (both are AgeRating enum values). The metadata is fetched and compared only when user.AgeRestriction != NotApplicable.","commonSituations":"A restricted/child account opens a Mature-rated series; the user's age restriction was lowered by an admin; series metadata AgeRating was upgraded after import; or a default age restriction policy applies to a new user.","solutions":["Raise the user's AgeRestriction (admin -> users) to a level that includes the series' rating.","Return 403 and hide the series from restricted users via the age-aware query.","Correct the series' AgeRating metadata if it was set too high during import.","Pre-filter browse/search results by AgeRating so restricted users never get a link to open it."],"exampleFix":"// before\nvar detail = await seriesService.GetSeriesDetail(seriesId, userId);\n\n// after\nvar user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct);\nvar meta = await unitOfWork.SeriesRepository.GetSeriesMetadataAsync(seriesId, ct);\nif (user!.AgeRestriction != AgeRating.NotApplicable && meta!.AgeRating > user.AgeRestriction)\n    return Forbid(\"series-restricted-age-restriction\");\nvar detail = await seriesService.GetSeriesDetail(seriesId, userId, ct);","handlingStrategy":"validation","validationCode":"var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct);\nvar meta = await unitOfWork.SeriesRepository.GetSeriesMetadataAsync(seriesId, ct);\nif (user!.AgeRestriction != AgeRating.NotApplicable && meta!.AgeRating > user.AgeRestriction)\n    return Forbid(\"series-restricted-age-restriction\");\nvar detail = await seriesService.GetSeriesDetail(seriesId, userId, ct);","typeGuard":"static bool AgeRatingAllowed(AgeRating restriction, AgeRating content) => restriction == AgeRating.NotApplicable || content <= restriction;","tryCatchPattern":"try { return await seriesService.GetSeriesDetail(seriesId, userId, ct); }\ncatch (UnauthorizedAccessException) { return Forbid(\"series-restricted-age-restriction\"); }","preventionTips":["Pre-filter browse/search by the user's AgeRestriction.","Adjust user AgeRestriction in admin when appropriate.","Correct inflated series AgeRating metadata."],"tags":["series","authorization","age-restriction","access-control"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}