Skip to content

Fix inspections/quickfix cache validation issue #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions rubberduckvba.Server/Api/Admin/AdminController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

namespace rubberduckvba.Server.Api.Admin;


[ApiController]
public class AdminController(ConfigurationOptions options, HangfireLauncherService hangfire, CacheService cache) : ControllerBase
{
Expand All @@ -15,7 +14,7 @@ public class AdminController(ConfigurationOptions options, HangfireLauncherServi
/// </summary>
/// <returns>The unique identifier of the enqueued job.</returns>
[Authorize("github")]
[EnableCors("CorsPolicy")]
[EnableCors(CorsPolicies.AllowAuthenticated)]
[HttpPost("admin/update/xmldoc")]
public IActionResult UpdateXmldocContent()
{
Expand All @@ -28,7 +27,7 @@ public IActionResult UpdateXmldocContent()
/// </summary>
/// <returns>The unique identifier of the enqueued job.</returns>
[Authorize("github")]
[EnableCors("CorsPolicy")]
[EnableCors(CorsPolicies.AllowAuthenticated)]
[HttpPost("admin/update/tags")]
public IActionResult UpdateTagMetadata()
{
Expand All @@ -37,7 +36,7 @@ public IActionResult UpdateTagMetadata()
}

[Authorize("github")]
[EnableCors("CorsPolicy")]
[EnableCors(CorsPolicies.AllowAuthenticated)]
[HttpPost("admin/cache/clear")]
public IActionResult ClearCache()
{
Expand All @@ -46,6 +45,8 @@ public IActionResult ClearCache()
}

#if DEBUG
[AllowAnonymous]
[EnableCors(CorsPolicies.AllowAll)]
[HttpGet("admin/config/current")]
public IActionResult Config()
{
Expand Down
2 changes: 1 addition & 1 deletion rubberduckvba.Server/Api/Admin/WebhookController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public WebhookController(
}

[Authorize("webhook")]
[EnableCors("webhookPolicy")]
[EnableCors(CorsPolicies.AllowAll)]
[HttpPost("webhook/github")]
public async Task<IActionResult> GitHub([FromBody] dynamic body) =>
GuardInternalAction(() =>
Expand Down
5 changes: 3 additions & 2 deletions rubberduckvba.Server/Api/Auth/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public AuthController(IOptions<GitHubSettings> configuration, IOptions<ApiSettin
}

[HttpGet("auth")]
[EnableCors(CorsPolicies.AllowAll)]
[AllowAnonymous]
public IActionResult Index()
{
Expand Down Expand Up @@ -71,7 +72,7 @@ public IActionResult Index()
}

[HttpPost("auth/signin")]
[EnableCors("CorsPolicy")]
[EnableCors(CorsPolicies.AllowAll)]
[AllowAnonymous]
public IActionResult SessionSignIn(SignInViewModel vm)
{
Expand Down Expand Up @@ -108,7 +109,7 @@ public IActionResult SessionSignIn(SignInViewModel vm)
}

[HttpPost("auth/github")]
[EnableCors("CorsPolicy")]
[EnableCors(CorsPolicies.AllowAll)]
[AllowAnonymous]
public IActionResult OnGitHubCallback(SignInViewModel vm)
{
Expand Down
2 changes: 2 additions & 0 deletions rubberduckvba.Server/Api/Downloads/DownloadsController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using rubberduckvba.Server.Services;
using System.Collections.Immutable;
Expand All @@ -7,6 +8,7 @@ namespace rubberduckvba.Server.Api.Downloads;


[AllowAnonymous]
[EnableCors(CorsPolicies.AllowAll)]
public class DownloadsController : RubberduckApiController
{
private readonly CacheService cache;
Expand Down
10 changes: 5 additions & 5 deletions rubberduckvba.Server/Api/Features/FeatureViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public FeatureViewModel(Feature model, bool summaryOnly = false)

public class InspectionViewModel
{
public InspectionViewModel(Inspection model, IDictionary<int, Tag> tagsByAssetId)
public InspectionViewModel(Inspection model, IEnumerable<QuickFixViewModel> quickFixes, IDictionary<int, Tag> tagsByAssetId)
{
Id = model.Id;
DateTimeInserted = model.DateTimeInserted;
Expand All @@ -78,7 +78,7 @@ public InspectionViewModel(Inspection model, IDictionary<int, Tag> tagsByAssetId

InspectionType = model.InspectionType;
DefaultSeverity = model.DefaultSeverity;
QuickFixes = model.QuickFixes;
QuickFixes = quickFixes.Where(e => model.QuickFixes.Any(name => string.Equals(e.Name, name, StringComparison.InvariantCultureIgnoreCase))).ToArray();

Reasoning = model.Reasoning;
HostApp = model.HostApp;
Expand Down Expand Up @@ -110,7 +110,7 @@ public InspectionViewModel(Inspection model, IDictionary<int, Tag> tagsByAssetId
public string? Remarks { get; init; }
public string? HostApp { get; init; }
public string[] References { get; init; } = [];
public string[] QuickFixes { get; init; } = [];
public QuickFixViewModel[] QuickFixes { get; init; } = [];
public InspectionExample[] Examples { get; init; } = [];
}

Expand Down Expand Up @@ -239,11 +239,11 @@ public record class QuickFixInspectionLinkViewModel

public class InspectionsFeatureViewModel : FeatureViewModel
{
public InspectionsFeatureViewModel(FeatureGraph model, IDictionary<int, Tag> tagsByAssetId, bool summaryOnly = false)
public InspectionsFeatureViewModel(FeatureGraph model, IEnumerable<QuickFixViewModel> quickFixes, IDictionary<int, Tag> tagsByAssetId, bool summaryOnly = false)
: base(model, summaryOnly)
{

Inspections = model.Inspections.OrderBy(e => e.Name).Select(e => new InspectionViewModel(e, tagsByAssetId)).ToArray();
Inspections = model.Inspections.OrderBy(e => e.Name).Select(e => new InspectionViewModel(e, quickFixes, tagsByAssetId)).ToArray();
}

public InspectionViewModel[] Inspections { get; init; } = [];
Expand Down
13 changes: 12 additions & 1 deletion rubberduckvba.Server/Api/Features/FeaturesController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using rubberduckvba.Server.Data;
using rubberduckvba.Server.Model;
Expand Down Expand Up @@ -37,6 +38,7 @@ await db.GetTopLevelFeatures(repositoryId)
.ContinueWith(t => t.Result.Select(e => new FeatureOptionViewModel { Id = e.Id, Name = e.Name, Title = e.Title }).ToArray());

[HttpGet("features")]
[EnableCors(CorsPolicies.AllowAll)]
[AllowAnonymous]
public IActionResult Index()
{
Expand Down Expand Up @@ -66,6 +68,7 @@ public IActionResult Index()
}

[HttpGet("features/{name}")]
[EnableCors(CorsPolicies.AllowAll)]
[AllowAnonymous]
public IActionResult Info([FromRoute] string name)
{
Expand All @@ -82,6 +85,7 @@ public IActionResult Info([FromRoute] string name)
}

[HttpGet("inspections/{name}")]
[EnableCors(CorsPolicies.AllowAll)]
[AllowAnonymous]
public IActionResult Inspection([FromRoute] string name)
{
Expand All @@ -103,6 +107,7 @@ public IActionResult Inspection([FromRoute] string name)
}

[HttpGet("annotations/{name}")]
[EnableCors(CorsPolicies.AllowAll)]
[AllowAnonymous]
public IActionResult Annotation([FromRoute] string name)
{
Expand All @@ -124,6 +129,7 @@ public IActionResult Annotation([FromRoute] string name)
}

[HttpGet("quickfixes/{name}")]
[EnableCors(CorsPolicies.AllowAll)]
[AllowAnonymous]
public IActionResult QuickFix([FromRoute] string name)
{
Expand All @@ -145,6 +151,7 @@ public IActionResult QuickFix([FromRoute] string name)
}

[HttpGet("features/create")]
[EnableCors(CorsPolicies.AllowAuthenticated)]
[Authorize("github")]
public async Task<ActionResult<FeatureEditViewModel>> Create([FromQuery] RepositoryId repository = RepositoryId.Rubberduck, [FromQuery] int? parentId = default)
{
Expand All @@ -156,6 +163,7 @@ public async Task<ActionResult<FeatureEditViewModel>> Create([FromQuery] Reposit
}

[HttpPost("create")]
[EnableCors(CorsPolicies.AllowAuthenticated)]
[Authorize("github")]
public async Task<ActionResult<FeatureEditViewModel>> Create([FromBody] FeatureEditViewModel model)
{
Expand All @@ -178,6 +186,7 @@ public async Task<ActionResult<FeatureEditViewModel>> Create([FromBody] FeatureE
}

[HttpPost("features/update")]
[EnableCors(CorsPolicies.AllowAuthenticated)]
[Authorize("github")]
public async Task<ActionResult<FeatureEditViewModel>> Update([FromBody] FeatureEditViewModel model)
{
Expand All @@ -203,8 +212,10 @@ private InspectionsFeatureViewModel GetInspections()
InspectionsFeatureViewModel result;
if (!cache.TryGetInspections(out result!))
{
var quickfixesModel = GetQuickFixes();

var feature = features.Get("Inspections") as FeatureGraph;
result = new InspectionsFeatureViewModel(feature,
result = new InspectionsFeatureViewModel(feature, quickfixesModel.QuickFixes,
feature.Inspections
.Select(e => e.TagAssetId).Distinct()
.ToDictionary(id => id, id => new Tag(tagsRepository.GetById(assetsRepository.GetById(id).TagId))));
Expand Down
2 changes: 2 additions & 0 deletions rubberduckvba.Server/Api/Indenter/IndenterController.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using RubberduckServices;

namespace rubberduckvba.Server.Api.Indenter;

[AllowAnonymous]
[EnableCors(CorsPolicies.AllowAll)]
public class IndenterController : RubberduckApiController
{
private readonly IIndenterService service;
Expand Down
3 changes: 2 additions & 1 deletion rubberduckvba.Server/Api/Tags/TagsController.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using rubberduckvba.Server.Services;

namespace rubberduckvba.Server.Api.Tags;


[AllowAnonymous]
[EnableCors(CorsPolicies.AllowAll)]
public class TagsController : RubberduckApiController
{
private readonly CacheService cache;
Expand All @@ -23,7 +25,6 @@ public TagsController(CacheService cache, IRubberduckDbService db, ILogger<TagsC
/// </summary>
[HttpGet("api/v1/public/tags")] // legacy route
[HttpGet("tags/latest")]
[AllowAnonymous]
public IActionResult Latest()
{
return GuardInternalAction(() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public SyncContext(IRequestParameters parameters)
IRequestParameters IPipelineContext.Parameters => Parameters;
public void LoadParameters(SyncRequestParameters parameters)
{
InvalidContextParameterException.ThrowIfNull(nameof(parameters), parameters);
_parameters = parameters;
_staging = new StagingContext(parameters);
}
Expand Down
4 changes: 2 additions & 2 deletions rubberduckvba.Server/GitHubAuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
var token = Context.Request.Headers["X-ACCESS-TOKEN"].SingleOrDefault();
if (string.IsNullOrWhiteSpace(token))
{
return AuthenticateResult.NoResult();
return AuthenticateResult.Fail("Access token was not provided");
}

var principal = await _github.ValidateTokenAsync(token);
Expand All @@ -36,7 +36,7 @@ protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
return AuthenticateResult.Success(new AuthenticationTicket(principal, "github"));
}

return AuthenticateResult.NoResult();
return AuthenticateResult.Fail("An invalid access token was provided");
}
catch (InvalidOperationException e)
{
Expand Down
19 changes: 17 additions & 2 deletions rubberduckvba.Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ public class HangfireAuthenticationFilter : IDashboardAuthorizationFilter
public bool Authorize([NotNull] DashboardContext context) => Debugger.IsAttached || context.Request.RemoteIpAddress == "20.220.30.154";
}

public static class CorsPolicies
{
public const string AllowAll = "AllowAll";
public const string AllowAuthenticated = "AllowAuthenticated";
}

public class Program
{
public static void Main(string[] args)
Expand All @@ -47,12 +53,20 @@ public static void Main(string[] args)

builder.Services.AddCors(builder =>
{
builder.AddPolicy("CorsPolicy", policy =>
builder.AddPolicy(CorsPolicies.AllowAll, policy =>
{
policy
.SetIsOriginAllowed(origin => true)
.AllowAnyHeader()
.WithMethods("OPTIONS", "GET", "POST")
.Build();
});
builder.AddPolicy(CorsPolicies.AllowAuthenticated, policy =>
{
policy
.SetIsOriginAllowed(origin => true)
.WithHeaders("X-ACCESS-TOKEN")
.WithMethods("OPTIONS", "GET", "POST")
.AllowCredentials()
.Build();
});
Expand Down Expand Up @@ -105,6 +119,7 @@ public static void Main(string[] args)

app.UseRouting();
app.UseCors();

app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
Expand All @@ -118,7 +133,7 @@ public static void Main(string[] args)
var hangfireOptions = app.Services.GetService<IOptions<HangfireSettings>>()?.Value ?? new();
new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions
{
Delay = TimeSpan.FromSeconds(10),
Delay = TimeSpan.FromSeconds(30),
MaxRetryAttempts = hangfireOptions.MaxInitializationAttempts,
OnRetry = (context) =>
{
Expand Down
12 changes: 6 additions & 6 deletions rubberduckvba.Server/Services/CacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public void Invalidate(FeatureViewModel[] newContent)
public void Invalidate(InspectionsFeatureViewModel newContent)
{
GetCurrentJobState();
if (XmldocJobState?.StateName == JobStateSucceeded)
if (!TryReadXmldocCache<InspectionsFeatureViewModel>("inspections", out _) || XmldocJobState?.StateName == JobStateSucceeded)
{
Write("inspections", newContent);
foreach (var item in newContent.Inspections)
Expand Down Expand Up @@ -171,10 +171,10 @@ private bool IsCacheValid(HangfireJobState? initial, Func<HangfireJobState?> get
GetCurrentJobState();
var current = getCurrent.Invoke();

return current != null // no job state -> no valid cache
&& current.StateName == JobStateSucceeded // last executed job must have succeeded
return current is null
|| (current.StateName == JobStateSucceeded // last executed job must have succeeded
&& current.LastJobId == initial?.LastJobId // last executed job must be the same job ID we know about
&& current.StateTimestamp == initial?.StateTimestamp; // same execution -> cache was not invalidated
&& current.StateTimestamp == initial?.StateTimestamp); // same execution -> cache was not invalidated
}

private void Write<T>(string key, T value)
Expand All @@ -185,15 +185,15 @@ private void Write<T>(string key, T value)

private bool TryReadFromTagsCache<T>(string key, out T? cached)
{
var result = _cache.TryGetValue(key, out cached) && IsTagsCacheValid();
var result = _cache.TryGetValue(key, out cached);
_logger.LogDebug("TagsCache hit: '{key}' (valid: {result})", key, result);

return result;
}

private bool TryReadXmldocCache<T>(string key, out T? cached)
{
var result = _cache.TryGetValue(key, out cached) && IsXmldocCacheValid();
var result = _cache.TryGetValue(key, out cached);
_logger.LogDebug("XmldocCache hit: '{key}' (valid: {result})", key, result);

return result;
Expand Down
9 changes: 5 additions & 4 deletions rubberduckvba.client/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,18 @@ export class LowerCaseUrlSerializer extends DefaultUrlSerializer {
BrowserModule,
FormsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
// legacy routes:
{ path: 'inspections/details/:name', redirectTo: 'inspections/:name' },
// actual routes:
{ path: 'auth/github', component: AuthComponent },
{ path: 'features', component: FeaturesComponent },
{ path: 'features/:name', component: FeatureComponent },
{ path: 'inspections/:name', component: InspectionComponent },
{ path: 'annotations/:name', component: AnnotationComponent },
{ path: 'quickfixes/:name', component: QuickFixComponent },
{ path: 'about', component: AboutComponent },
{ path: 'auth/github', component: AuthComponent },
{ path: 'indenter', component: IndenterComponent },
// legacy routes:
{ path: 'inspections/details/:name', redirectTo: 'inspections/:name' },
{ path: '', component: HomeComponent, pathMatch: 'full' },
]),
FontAwesomeModule,
NgbModule
Expand Down
Loading