From 2e24bd27759a4a21241d66fbf646d8cc90d49966 Mon Sep 17 00:00:00 2001 From: Mathieu Guindon Date: Sat, 8 Feb 2025 22:59:40 -0500 Subject: [PATCH 1/4] ws --- rubberduckvba.client/src/app/routes/home/home.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/rubberduckvba.client/src/app/routes/home/home.component.html b/rubberduckvba.client/src/app/routes/home/home.component.html index 7a46ac3..fb5610e 100644 --- a/rubberduckvba.client/src/app/routes/home/home.component.html +++ b/rubberduckvba.client/src/app/routes/home/home.component.html @@ -161,4 +161,3 @@

Resources

- From 06eded830c17b0f6614a9f8a3d9b13d82a33db4f Mon Sep 17 00:00:00 2001 From: Mathieu Guindon Date: Sat, 8 Feb 2025 23:15:40 -0500 Subject: [PATCH 2/4] fix hscroll --- .../src/app/app.component.html | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/rubberduckvba.client/src/app/app.component.html b/rubberduckvba.client/src/app/app.component.html index 36f9caa..ad19940 100644 --- a/rubberduckvba.client/src/app/app.component.html +++ b/rubberduckvba.client/src/app/app.component.html @@ -1,11 +1,17 @@ - -
- -
-
-
+ +
+ +
+
+ +
+
+ +
+
+
© 2014-{{currentYear}} Rubberduck Project Contributors
From 920b6ae4cfb08e7c412bdda276cdc254d989f996 Mon Sep 17 00:00:00 2001 From: Mathieu Guindon Date: Mon, 10 Feb 2025 23:40:16 -0500 Subject: [PATCH 3/4] wip --- .../Api/Admin/WebhookController.cs | 61 ++++--- .../Api/Auth/AuthController.cs | 159 ++++++++++-------- .../Api/Indenter/IndenterController.cs | 63 +++++++ rubberduckvba.Server/Program.cs | 64 +++++-- .../RubberduckApiController.cs | 34 +--- .../rubberduckvba.Server.csproj | 6 + rubberduckvba.client/src/app/app.component.ts | 1 - rubberduckvba.client/src/app/app.module.ts | 25 +-- .../auth-menu/auth-menu.component.ts | 7 +- .../loading-content.component.ts | 9 +- .../nav-menu/nav-menu.component.html | 3 + .../components/nav-menu/nav-menu.component.ts | 7 +- .../src/app/model/indenter.model.ts | 122 ++++++++++++++ .../src/app/routes/about/about.component.html | 4 +- .../routes/indenter/indenter.component.css | 0 .../routes/indenter/indenter.component.html | 109 ++++++++++++ .../app/routes/indenter/indenter.component.ts | 47 ++++++ .../src/app/services/api-client.service.ts | 21 ++- .../src/app/services/data.service.ts | 19 ++- 19 files changed, 575 insertions(+), 186 deletions(-) create mode 100644 rubberduckvba.Server/Api/Indenter/IndenterController.cs create mode 100644 rubberduckvba.client/src/app/model/indenter.model.ts create mode 100644 rubberduckvba.client/src/app/routes/indenter/indenter.component.css create mode 100644 rubberduckvba.client/src/app/routes/indenter/indenter.component.html create mode 100644 rubberduckvba.client/src/app/routes/indenter/indenter.component.ts diff --git a/rubberduckvba.Server/Api/Admin/WebhookController.cs b/rubberduckvba.Server/Api/Admin/WebhookController.cs index 3015ceb..8f99aab 100644 --- a/rubberduckvba.Server/Api/Admin/WebhookController.cs +++ b/rubberduckvba.Server/Api/Admin/WebhookController.cs @@ -22,36 +22,35 @@ public WebhookController( [Authorize("webhook")] [HttpPost("webhook/github")] - public async Task GitHub([FromBody] dynamic body) - { - //var reader = new StreamReader(Request.Body); - //var json = await reader.ReadToEndAsync(); - string json = body.ToString(); - - var payload = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) - ?? throw new InvalidOperationException("Could not deserialize payload"); - var eventType = _validator.Validate(payload, json, Request.Headers, out var content, out var gitref); - - if (eventType == WebhookPayloadType.Push) - { - var jobId = _hangfire.UpdateXmldocContent(); - var message = $"Webhook push event was accepted. Tag '{gitref?.Name}' associated to the payload will be processed by JobId '{jobId}'."; - - Logger.LogInformation(message); - return Ok(message); - } - else if (eventType == WebhookPayloadType.Ping) - { - Logger.LogInformation("Webhook ping event was accepted; nothing to process."); - return Ok(); - } - else if (eventType == WebhookPayloadType.Greeting) + public async Task GitHub([FromBody] dynamic body) => + GuardInternalAction(() => { - Logger.LogInformation("Webhook push event was accepted; nothing to process. {content}", content); - return string.IsNullOrWhiteSpace(content) ? NoContent() : Ok(content); - } - - // reject the payload - return BadRequest(); - } + string json = body.ToString(); + + var payload = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) + ?? throw new InvalidOperationException("Could not deserialize payload"); + var eventType = _validator.Validate(payload, json, Request.Headers, out var content, out var gitref); + + if (eventType == WebhookPayloadType.Push) + { + var jobId = _hangfire.UpdateXmldocContent(); + var message = $"Webhook push event was accepted. Tag '{gitref?.Name}' associated to the payload will be processed by JobId '{jobId}'."; + + Logger.LogInformation(message); + return Ok(message); + } + else if (eventType == WebhookPayloadType.Ping) + { + Logger.LogInformation("Webhook ping event was accepted; nothing to process."); + return Ok(); + } + else if (eventType == WebhookPayloadType.Greeting) + { + Logger.LogInformation("Webhook push event was accepted; nothing to process. {content}", content); + return string.IsNullOrWhiteSpace(content) ? NoContent() : Ok(content); + } + + // reject the payload + return BadRequest(); + }); } diff --git a/rubberduckvba.Server/Api/Auth/AuthController.cs b/rubberduckvba.Server/Api/Auth/AuthController.cs index 3e76251..05a8818 100644 --- a/rubberduckvba.Server/Api/Auth/AuthController.cs +++ b/rubberduckvba.Server/Api/Auth/AuthController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Octokit; using Octokit.Internal; @@ -23,91 +24,113 @@ public record class SignInViewModel } [ApiController] -public class AuthController(IOptions configuration, IOptions api, ILogger logger) : ControllerBase +[AllowAnonymous] +public class AuthController : RubberduckApiController { + private readonly IOptions configuration; + + public AuthController(IOptions configuration, IOptions api, ILogger logger) + : base(logger) + { + this.configuration = configuration; + } + [HttpGet("auth")] + [AllowAnonymous] public IActionResult Index() { - var claims = HttpContext.User.Claims.ToDictionary(claim => claim.Type, claim => claim.Value); - var hasName = claims.TryGetValue(ClaimTypes.Name, out var name); - var hasRole = claims.TryGetValue(ClaimTypes.Role, out var role); - - if (hasName && hasRole) + return GuardInternalAction(() => { - if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(role)) + var claims = HttpContext.User.Claims.ToDictionary(claim => claim.Type, claim => claim.Value); + var hasName = claims.TryGetValue(ClaimTypes.Name, out var name); + var hasRole = claims.TryGetValue(ClaimTypes.Role, out var role); + + if (hasName && hasRole) { - return BadRequest(); + if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(role)) + { + return BadRequest(); + } + + var isAuthenticated = HttpContext.User.Identity?.IsAuthenticated ?? false; + var model = new UserViewModel + { + Name = name, + IsAuthenticated = isAuthenticated, + IsAdmin = role == configuration.Value.OwnerOrg + }; + + return Ok(model); } - - var isAuthenticated = HttpContext.User.Identity?.IsAuthenticated ?? false; - var model = new UserViewModel + else { - Name = name, - IsAuthenticated = isAuthenticated, - IsAdmin = role == configuration.Value.OwnerOrg - }; - - return Ok(model); - } - else - { - return Ok(UserViewModel.Anonymous); - } + return Ok(UserViewModel.Anonymous); + } + }); } [HttpPost("auth/signin")] + [AllowAnonymous] public IActionResult SessionSignIn(SignInViewModel vm) { - if (User.Identity?.IsAuthenticated ?? false) + return GuardInternalAction(() => { - logger.LogInformation("Signin was requested, but user is already authenticated. Redirecting to home page..."); - return Redirect("/"); - } + if (User.Identity?.IsAuthenticated ?? false) + { + Logger.LogInformation("Signin was requested, but user is already authenticated. Redirecting to home page..."); + return Redirect("/"); + } - var clientId = configuration.Value.ClientId; - var agent = configuration.Value.UserAgent; + var clientId = configuration.Value.ClientId; + var agent = configuration.Value.UserAgent; - var github = new GitHubClient(new ProductHeaderValue(agent)); - var request = new OauthLoginRequest(clientId) - { - AllowSignup = false, - Scopes = { "read:user", "read:org" }, - State = vm.State - }; - - logger.LogInformation("Requesting OAuth app GitHub login url..."); - var url = github.Oauth.GetGitHubLoginUrl(request); - if (url is null) - { - logger.LogInformation("OAuth login was cancelled by the user or did not return a url."); - return Forbid(); - } + var github = new GitHubClient(new ProductHeaderValue(agent)); + var request = new OauthLoginRequest(clientId) + { + AllowSignup = false, + Scopes = { "read:user", "read:org" }, + State = vm.State + }; - logger.LogInformation("Returning the login url for the client to redirect. State: {xsrf}", vm.State); - return Ok(url.ToString()); + Logger.LogInformation("Requesting OAuth app GitHub login url..."); + var url = github.Oauth.GetGitHubLoginUrl(request); + if (url is null) + { + Logger.LogInformation("OAuth login was cancelled by the user or did not return a url."); + return Forbid(); + } + + Logger.LogInformation("Returning the login url for the client to redirect. State: {xsrf}", vm.State); + return Ok(url.ToString()); + }); } [HttpPost("auth/github")] - public async Task OnGitHubCallback(SignInViewModel vm) + [AllowAnonymous] + public IActionResult OnGitHubCallback(SignInViewModel vm) { - logger.LogInformation("OAuth token was received. State: {state}", vm.State); - var clientId = configuration.Value.ClientId; - var clientSecret = configuration.Value.ClientSecret; - var agent = configuration.Value.UserAgent; + return GuardInternalAction(() => + { + Logger.LogInformation("OAuth token was received. State: {state}", vm.State); + var clientId = configuration.Value.ClientId; + var clientSecret = configuration.Value.ClientSecret; + var agent = configuration.Value.UserAgent; - var github = new GitHubClient(new ProductHeaderValue(agent)); + var github = new GitHubClient(new ProductHeaderValue(agent)); - var request = new OauthTokenRequest(clientId, clientSecret, vm.Code); - var token = await github.Oauth.CreateAccessToken(request); - if (token is null) - { - logger.LogWarning("OAuth access token was not created."); - return Unauthorized(); - } + var request = new OauthTokenRequest(clientId, clientSecret, vm.Code); + var token = github.Oauth.CreateAccessToken(request).GetAwaiter().GetResult(); + if (token is null) + { + Logger.LogWarning("OAuth access token was not created."); + return Unauthorized(); + } + + Logger.LogInformation("OAuth access token was created. Authorizing..."); + var authorizedToken = AuthorizeAsync(token.AccessToken).GetAwaiter().GetResult(); - logger.LogInformation("OAuth access token was created. Authorizing..."); - var authorizedToken = await AuthorizeAsync(token.AccessToken); - return authorizedToken is null ? Unauthorized() : Ok(vm with { Token = authorizedToken }); + return authorizedToken is null ? Unauthorized() : Ok(vm with { Token = authorizedToken }); + }); } private async Task AuthorizeAsync(string token) @@ -119,13 +142,13 @@ public async Task OnGitHubCallback(SignInViewModel vm) var githubUser = await github.User.Current(); if (githubUser.Suspended) { - logger.LogWarning("User {name} with login '{login}' ({url}) is a suspended GitHub account and will not be authorized.", githubUser.Name, githubUser.Login, githubUser.Url); + Logger.LogWarning("User login '{login}' ({name}) is a suspended GitHub account and will not be authorized.", githubUser.Login, githubUser.Name); return default; } var identity = new ClaimsIdentity("github", ClaimTypes.Name, ClaimTypes.Role); identity.AddClaim(new Claim(ClaimTypes.Name, githubUser.Login)); - logger.LogInformation("Creating claims identity for GitHub login '{login}'...", githubUser.Login); + Logger.LogInformation("Creating claims identity for GitHub login '{login}'...", githubUser.Login); var orgs = await github.Organization.GetAllForUser(githubUser.Login); var rdOrg = orgs.SingleOrDefault(org => org.Id == configuration.Value.RubberduckOrgId); @@ -135,7 +158,7 @@ public async Task OnGitHubCallback(SignInViewModel vm) identity.AddClaim(new Claim(ClaimTypes.Role, configuration.Value.OwnerOrg)); identity.AddClaim(new Claim(ClaimTypes.Authentication, token)); identity.AddClaim(new Claim("access_token", token)); - logger.LogDebug("GitHub Organization claims were granted. Creating claims principal..."); + Logger.LogDebug("GitHub Organization claims were granted. Creating claims principal..."); var principal = new ClaimsPrincipal(identity); var roles = string.Join(",", identity.Claims.Where(claim => claim.Type == ClaimTypes.Role).Select(claim => claim.Value)); @@ -143,19 +166,19 @@ public async Task OnGitHubCallback(SignInViewModel vm) HttpContext.User = principal; Thread.CurrentPrincipal = HttpContext.User; - logger.LogInformation("GitHub user with login {login} has signed in with role authorizations '{role}'.", githubUser.Login, configuration.Value.OwnerOrg); + Logger.LogInformation("GitHub user with login {login} has signed in with role authorizations '{role}'.", githubUser.Login, configuration.Value.OwnerOrg); return token; } else { - logger.LogWarning("User {name} ({email}) with login '{login}' is not a member of organization ID {org} and will not be authorized.", githubUser.Name, githubUser.Email, githubUser.Login, configuration.Value.RubberduckOrgId); + Logger.LogWarning("User {name} ({email}) with login '{login}' is not a member of organization ID {org} and will not be authorized.", githubUser.Name, githubUser.Email, githubUser.Login, configuration.Value.RubberduckOrgId); return default; } } catch (Exception) { // just ignore: configuration needs the org (prod) client app id to avoid throwing this exception - logger.LogWarning("An exception was thrown. Verify GitHub:ClientId and GitHub:ClientSecret configuration; authorization fails."); + Logger.LogWarning("An exception was thrown. Verify GitHub:ClientId and GitHub:ClientSecret configuration; authorization fails."); return default; } } diff --git a/rubberduckvba.Server/Api/Indenter/IndenterController.cs b/rubberduckvba.Server/Api/Indenter/IndenterController.cs new file mode 100644 index 0000000..80f59b4 --- /dev/null +++ b/rubberduckvba.Server/Api/Indenter/IndenterController.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RubberduckServices; + +namespace rubberduckvba.Server.Api.Indenter; + +[AllowAnonymous] +public class IndenterController : RubberduckApiController +{ + private readonly IIndenterService service; + + public IndenterController(IIndenterService service, ILogger logger) + : base(logger) + { + this.service = service; + } + + [HttpGet("indenter/version")] + [AllowAnonymous] + public IActionResult Version() => + GuardInternalAction(() => Ok(service.IndenterVersion())); + + [HttpGet("indenter/defaults")] + [AllowAnonymous] + public IActionResult DefaultSettings() => + GuardInternalAction(() => + { + var result = new IndenterViewModel + { + IndenterVersion = service.IndenterVersion(), + Code = "Option Explicit\n\n'...comments...\n\nPublic Sub DoSomething()\n'...comments...\n\nEnd Sub\nPublic Sub DoSomethingElse()\n'...comments...\n\nIf True Then\nMsgBox \"Hello, world!\"\nElse\n'...comments...\nExit Sub\nEnd If\nEnd Sub\n", + AlignCommentsWithCode = true, + EmptyLineHandlingMethod = IndenterEmptyLineHandling.Indent, + ForceCompilerDirectivesInColumn1 = true, + GroupRelatedProperties = false, + IndentSpaces = 4, + IndentCase = true, + IndentEntireProcedureBody = true, + IndentEnumTypeAsProcedure = true, + VerticallySpaceProcedures = true, + LinesBetweenProcedures = 1, + IndentFirstCommentBlock = true, + IndentFirstDeclarationBlock = true, + EndOfLineCommentStyle = IndenterEndOfLineCommentStyle.SameGap, + }; + + return Ok(result); + }); + + [HttpPost("indenter/indent")] + [AllowAnonymous] + public IActionResult Indent(IndenterViewModel model) => + GuardInternalAction(() => + { + if (model is null) + { + throw new ArgumentNullException(nameof(model)); + } + + var result = service.IndentAsync(model).GetAwaiter().GetResult(); + return Ok(result); + }); +} diff --git a/rubberduckvba.Server/Program.cs b/rubberduckvba.Server/Program.cs index e867d05..b4eeace 100644 --- a/rubberduckvba.Server/Program.cs +++ b/rubberduckvba.Server/Program.cs @@ -7,6 +7,7 @@ using NLog.Config; using NLog.Extensions.Logging; using NLog.Targets; +using Rubberduck.SmartIndenter; using RubberduckServices; using rubberduckvba.Server.Api.Admin; using rubberduckvba.Server.ContentSynchronization; @@ -42,6 +43,55 @@ public static void Main(string[] args) builder.Services.Configure(options => builder.Configuration.GetSection("Api").Bind(options)); builder.Services.Configure(options => builder.Configuration.GetSection("Hangfire").Bind(options)); + builder.Services.AddCors(builder => + { + builder.AddDefaultPolicy(policy => + { + policy + .SetIsOriginAllowed(origin => true) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials() + .Build(); + }); + /* + var guestPolicy = new CorsPolicyBuilder() + .SetIsOriginAllowed(origin => true) + .WithHeaders("Content-Type", "Accept") + .WithExposedHeaders("X-ACCESS-TOKEN") + .WithMethods("GET", "POST", "OPTIONS") + .DisallowCredentials() + .SetPreflightMaxAge(TimeSpan.FromHours(48)) + .Build(); + //builder.AddDefaultPolicy(guestPolicy); + //builder.DefaultPolicyName = nameof(guestPolicy); + builder.AddPolicy(nameof(guestPolicy), guestPolicy); + + var webhookPolicy = new CorsPolicyBuilder() + #if DEBUG + .SetIsOriginAllowed(origin => true) + #else + .SetIsOriginAllowedToAllowWildcardSubdomains() + .WithOrigins("*.github.com") + #endif + .WithHeaders("Content-Type", "X-GitHub-Event", "X-GitHub-Delivery", "X-GitHub-Hook-ID", "X-Hub-Signature", "X-Hub-Signature256") + .WithMethods("POST") + .DisallowCredentials() + .SetPreflightMaxAge(TimeSpan.FromHours(48)) + .Build(); + builder.AddPolicy(nameof(webhookPolicy), webhookPolicy); + + var adminPolicy = new CorsPolicyBuilder() + .SetIsOriginAllowed(origin => true) + .WithHeaders("Content-Type", "Authorization") + .WithExposedHeaders("X-ACCESS-TOKEN") + .WithMethods("GET", "POST") + .SetPreflightMaxAge(TimeSpan.FromHours(48)) + .Build(); + builder.AddPolicy(nameof(adminPolicy), adminPolicy); + */ + }); + builder.Services.AddAuthentication(options => { options.RequireAuthenticatedSignIn = false; @@ -88,22 +138,14 @@ public static void Main(string[] args) app.UseHttpsRedirection(); app.UseRouting(); - app.UseSession(); + app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); + app.UseSession(); app.MapControllers(); app.MapFallbackToFile("/index.html"); - app.UseCors(policy => - { - policy - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials() - .SetIsOriginAllowed(origin => true); - }); - StartHangfire(app); app.Run(); } @@ -162,6 +204,8 @@ private static void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/rubberduckvba.Server/RubberduckApiController.cs b/rubberduckvba.Server/RubberduckApiController.cs index 3e5125c..2f40dbf 100644 --- a/rubberduckvba.Server/RubberduckApiController.cs +++ b/rubberduckvba.Server/RubberduckApiController.cs @@ -17,39 +17,6 @@ protected RubberduckApiController(ILogger logger) protected ILogger Logger => _logger; - - protected async Task GuardInternalActionAsync(Func> method, [CallerMemberName] string name = default!) - { - var sw = Stopwatch.StartNew(); - IActionResult result = NoContent(); - var success = false; - try - { - _logger.LogTrace("GuardInternalActionAsync:{name} | ▶ Invoking controller action", name); - result = await method.Invoke(); - success = true; - } - catch (Exception exception) - { - _logger.LogError(exception, "GuardInternalActionAsync:{name} | ❌ An exception was caught", name); - throw; - } - finally - { - sw.Stop(); - if (success) - { - _logger.LogTrace("GuardInternalActionAsync:{name} | ✔️ Controller action completed | ⏱️ {elapsed}", name, sw.Elapsed); - } - else - { - _logger.LogWarning("GuardInternalActionAsync:{name} | ⚠️ Controller action completed with errors", name); - } - } - - return result; - } - protected IActionResult GuardInternalAction(Func method, [CallerMemberName] string name = default!) { var sw = Stopwatch.StartNew(); @@ -79,6 +46,7 @@ protected IActionResult GuardInternalAction(Func method, [CallerM } } + //Response.Headers.AccessControlAllowOrigin = "*"; return result; } } diff --git a/rubberduckvba.Server/rubberduckvba.Server.csproj b/rubberduckvba.Server/rubberduckvba.Server.csproj index e7a6e35..7af4d85 100644 --- a/rubberduckvba.Server/rubberduckvba.Server.csproj +++ b/rubberduckvba.Server/rubberduckvba.Server.csproj @@ -41,4 +41,10 @@ + + + ..\RubberduckServices\Libs\Rubberduck.SmartIndenter.dll + + + diff --git a/rubberduckvba.client/src/app/app.component.ts b/rubberduckvba.client/src/app/app.component.ts index 5d8c5a9..58dc64c 100644 --- a/rubberduckvba.client/src/app/app.component.ts +++ b/rubberduckvba.client/src/app/app.component.ts @@ -1,4 +1,3 @@ -import { HttpClient } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; @Component({ diff --git a/rubberduckvba.client/src/app/app.module.ts b/rubberduckvba.client/src/app/app.module.ts index 0427b9a..1c412ae 100644 --- a/rubberduckvba.client/src/app/app.module.ts +++ b/rubberduckvba.client/src/app/app.module.ts @@ -1,16 +1,19 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -import { DataService } from './services/data.service'; -import { AdminApiClientService, ApiClientService } from './services/api-client.service'; +import { FormsModule } from '@angular/forms'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { BrowserModule } from '@angular/platform-browser'; import { RouterModule, UrlSerializer } from '@angular/router'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +import { DataService } from './services/data.service'; +import { ApiClientService } from './services/api-client.service'; + import { AppComponent } from './app.component'; import { NavMenuComponent } from './components/nav-menu/nav-menu.component'; +import { LoadingContentComponent } from './components/loading-content/loading-content.component'; import { TagDownloadComponent } from './components/tag-download/tag-download.component'; import { DownloadItemComponent } from './components/download-item/download-item.component'; import { FeatureBoxComponent } from './components/feature-box/feature-box.component'; @@ -18,7 +21,6 @@ import { FeatureInfoComponent } from './components/feature-info/feature-info.com import { FeatureItemBoxComponent } from './components/feature-item-box/feature-item-box.component'; import { ExampleBoxComponent } from './components/example-box/example-box.component'; import { FeatureItemExampleComponent } from './components/quickfix-example.modal/quickfix-example.modal.component'; -import { LoadingContentComponent } from './components/loading-content/loading-content.component'; import { InspectionItemBoxComponent } from './components/feature-item-box/inspection-item-box/inspection-item-box.component'; import { AnnotationItemBoxComponent } from './components/feature-item-box/annotation-item-box/annotation-item-box.component'; import { BlogLinkBoxComponent } from './components/blog-link-box/blog-link-box.component'; @@ -31,10 +33,11 @@ import { FeatureComponent } from './routes/feature/feature.component'; import { InspectionComponent } from './routes/inspection/inspection.component'; import { AnnotationComponent } from './routes/annotation/annotation.component'; import { QuickFixComponent } from './routes/quickfixes/quickfix.component'; +import { IndenterComponent } from './routes/indenter/indenter.component'; import { DefaultUrlSerializer, UrlTree } from '@angular/router'; -import { AuthMenuComponent } from './components/auth-menu/auth-menu.component'; import { AuthComponent } from './routes/auth/auth.component'; +import { AuthMenuComponent } from './components/auth-menu/auth-menu.component'; /** * https://stackoverflow.com/a/39560520 @@ -55,6 +58,8 @@ export class LowerCaseUrlSerializer extends DefaultUrlSerializer { AppComponent, HomeComponent, AuthComponent, + AuthMenuComponent, + IndenterComponent, FeaturesComponent, FeatureComponent, TagDownloadComponent, @@ -73,13 +78,13 @@ export class LowerCaseUrlSerializer extends DefaultUrlSerializer { InspectionComponent, AnnotationComponent, QuickFixComponent, - AboutComponent, - AuthMenuComponent + AboutComponent ], bootstrap: [AppComponent], imports: [ - BrowserModule, CommonModule, + BrowserModule, + FormsModule, RouterModule.forRoot([ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'features', component: FeaturesComponent }, @@ -89,6 +94,7 @@ export class LowerCaseUrlSerializer extends DefaultUrlSerializer { { 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' }, ]), @@ -98,7 +104,6 @@ export class LowerCaseUrlSerializer extends DefaultUrlSerializer { providers: [ DataService, ApiClientService, - AdminApiClientService, provideHttpClient(withInterceptorsFromDi()), { provide: UrlSerializer, diff --git a/rubberduckvba.client/src/app/components/auth-menu/auth-menu.component.ts b/rubberduckvba.client/src/app/components/auth-menu/auth-menu.component.ts index eb1c88b..672b29c 100644 --- a/rubberduckvba.client/src/app/components/auth-menu/auth-menu.component.ts +++ b/rubberduckvba.client/src/app/components/auth-menu/auth-menu.component.ts @@ -1,4 +1,3 @@ -/// import { Component, OnInit, TemplateRef, ViewChild, inject } from "@angular/core"; import { FaIconLibrary } from "@fortawesome/angular-fontawesome"; import { BehaviorSubject } from "rxjs"; @@ -6,8 +5,8 @@ import { UserViewModel } from "../../model/feature.model"; import { AuthService } from "src/app/services/auth.service"; import { fas } from "@fortawesome/free-solid-svg-icons"; import { fab } from "@fortawesome/free-brands-svg-icons"; -import { NgbModal, NgbToast } from "@ng-bootstrap/ng-bootstrap"; -import { AdminApiClientService, ApiClientService } from "../../services/api-client.service"; +import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; +import { ApiClientService } from "../../services/api-client.service"; @Component({ selector: 'auth-menu', @@ -29,7 +28,7 @@ export class AuthMenuComponent implements OnInit { @ViewChild('confirmxmldocsbox', { read: TemplateRef }) confirmxmldocsbox: TemplateRef | undefined; public modal = inject(NgbModal); - constructor(private auth: AuthService, private api: AdminApiClientService, private fa: FaIconLibrary) { + constructor(private auth: AuthService, private api: ApiClientService, private fa: FaIconLibrary) { fa.addIconPacks(fas); fa.addIconPacks(fab); } diff --git a/rubberduckvba.client/src/app/components/loading-content/loading-content.component.ts b/rubberduckvba.client/src/app/components/loading-content/loading-content.component.ts index 65ec935..6e20f5a 100644 --- a/rubberduckvba.client/src/app/components/loading-content/loading-content.component.ts +++ b/rubberduckvba.client/src/app/components/loading-content/loading-content.component.ts @@ -1,11 +1,4 @@ -import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChange, SimpleChanges, TemplateRef, ViewChild, inject } from '@angular/core'; -import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; -import { fas } from '@fortawesome/free-solid-svg-icons'; -import { fab } from '@fortawesome/free-brands-svg-icons'; -import { Tag, TagDownloadInfo } from '../../model/tags.model'; -import { BehaviorSubject } from 'rxjs'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AngularDeviceInformationService } from 'angular-device-information'; +import { Component, Input } from '@angular/core'; @Component({ //standalone: true, diff --git a/rubberduckvba.client/src/app/components/nav-menu/nav-menu.component.html b/rubberduckvba.client/src/app/components/nav-menu/nav-menu.component.html index f9f1a98..c63520a 100644 --- a/rubberduckvba.client/src/app/components/nav-menu/nav-menu.component.html +++ b/rubberduckvba.client/src/app/components/nav-menu/nav-menu.component.html @@ -18,6 +18,9 @@ +
  • -  Smart Indenter +  Smart Indenter

    The original Smart Indenter VBIDE add-in VB6 source code that served as a reference.

    diff --git a/rubberduckvba.client/src/app/routes/indenter/indenter.component.css b/rubberduckvba.client/src/app/routes/indenter/indenter.component.css new file mode 100644 index 0000000..e69de29 diff --git a/rubberduckvba.client/src/app/routes/indenter/indenter.component.html b/rubberduckvba.client/src/app/routes/indenter/indenter.component.html new file mode 100644 index 0000000..f110fce --- /dev/null +++ b/rubberduckvba.client/src/app/routes/indenter/indenter.component.html @@ -0,0 +1,109 @@ + +
    +
    +

    Online Indenter

    +

    + Consistent identation is one of the most fundemntal qualities of code that is clean and clear. + Rubberduck makes indenting a procedure, module, or even an entire project, quick and simple. +

    +

    + With all the functionalities of the legendary 32-bit Smart Indenter add-in, and then some, + Rubberduck lets you configure many indenting options: feel free to use this page to experiment with them. +

    +
    +
    + +
    + + +
    +
    +
    + +
    + something +
    +
    +
    + +
    +
    + something +
    +
    +
    +
    + +
    +
    + something +
    +
    +
    +
    + +
    +
    + something +
    +
    +
    +
    + +
    +
    + something +
    +
    +
    +
    +
    + +
    +
    +
    +

    Try it right here!

    + Rubberduck.SmartIndenter.dll version: {{model.indenterVersion}} +
    +
    + + +
    + +
    +
    + + + + + + + +
    + +
    + +
    diff --git a/rubberduckvba.client/src/app/routes/indenter/indenter.component.ts b/rubberduckvba.client/src/app/routes/indenter/indenter.component.ts new file mode 100644 index 0000000..5980645 --- /dev/null +++ b/rubberduckvba.client/src/app/routes/indenter/indenter.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from "@angular/core"; +import { FaIconLibrary } from "@fortawesome/angular-fontawesome"; +import { fas } from "@fortawesome/free-solid-svg-icons"; +import { IndenterViewModel } from "../../model/indenter.model"; +import { ApiClientService } from "../../services/api-client.service"; + +@Component({ + selector: 'app-indenter', + templateUrl: './indenter.component.html', +}) +export class IndenterComponent implements OnInit { + private _model!: IndenterViewModel; + + constructor(fa: FaIconLibrary, private service: ApiClientService) { + fa.addIconPacks(fas); + } + + ngOnInit(): void { + this.service.getIndenterDefaults().subscribe(model => { + this._model = model; + }); + } + + public isExpanded: boolean = false; + public isIndenterBusy: boolean = false; + + public get model(): IndenterViewModel { + return this._model; + } + + public indent(): void { + this.isIndenterBusy = true; + this.service.indent(this.model).subscribe(vm => { + this.model.indentedCode = vm.indentedCode; + this.model.code = vm.indentedCode; + this.isIndenterBusy = false; + }); + } + + public clear(): void { + this.model.code = ''; + } + + public onModelChanged(code: string): void { + this.model.code = code; + } +} diff --git a/rubberduckvba.client/src/app/services/api-client.service.ts b/rubberduckvba.client/src/app/services/api-client.service.ts index 69584e0..c6ea06c 100644 --- a/rubberduckvba.client/src/app/services/api-client.service.ts +++ b/rubberduckvba.client/src/app/services/api-client.service.ts @@ -5,6 +5,7 @@ import { DownloadInfo } from "../model/downloads.model"; import { DataService } from "./data.service"; import { environment } from "../../environments/environment.prod"; import { Observable, map } from "rxjs"; +import { IndenterVersionViewModelClass, IndenterViewModel, IndenterViewModelClass } from "../model/indenter.model"; @Injectable() export class ApiClientService { @@ -53,13 +54,6 @@ export class ApiClientService { const url = `${environment.apiBaseUrl}quickfixes/${name}` return this.data.getAsync(url).pipe(map(e => new QuickFixViewModelClass(e))); } -} - -@Injectable() -export class AdminApiClientService { - - constructor(private data: DataService) { - } public updateTagMetadata(): Observable { const url = `${environment.apiBaseUrl}admin/update/tags`; @@ -70,4 +64,17 @@ export class AdminApiClientService { const url = `${environment.apiBaseUrl}admin/update/xmldoc`; return this.data.postAsync(url); } + + public getIndenterDefaults(): Observable { + const url = `${environment.apiBaseUrl}indenter/defaults`; + return this.data.getAsync(url).pipe(map(model => new IndenterViewModelClass(model))); + } + + public indent(model: IndenterViewModel): Observable { + const url = `${environment.apiBaseUrl}indenter/indent`; + return this.data.postAsync(url, model).pipe(map(lines => { + model.indentedCode = lines.join('\n'); + return model; + })); + } } diff --git a/rubberduckvba.client/src/app/services/data.service.ts b/rubberduckvba.client/src/app/services/data.service.ts index 9721dd5..4b1f094 100644 --- a/rubberduckvba.client/src/app/services/data.service.ts +++ b/rubberduckvba.client/src/app/services/data.service.ts @@ -1,7 +1,6 @@ import { HttpClient, HttpHeaders } from "@angular/common/http"; -import { Injectable, Query } from "@angular/core"; +import { Injectable } from "@angular/core"; import { map, timeout, catchError, throwError, Observable } from "rxjs"; -import { ApiClientService } from "./api-client.service"; @Injectable() export class DataService { @@ -14,14 +13,14 @@ export class DataService { public getAsync(url: string): Observable { let headers = new HttpHeaders() .append('accept', 'application/json'); - const token = sessionStorage.getItem('github:access_token'); + let withCreds = false; if (token) { - headers = headers.append('X-ACCESS-TOKEN', token) - .append('Access-Control-Allow-Origin', '*'); + headers = headers.append('X-ACCESS-TOKEN', token); + withCreds = true; } - return this.http.get(url, { headers }) + return this.http.get(url, { headers, withCredentials: withCreds }) .pipe( map(result => result), timeout(this.timeout), @@ -34,17 +33,19 @@ export class DataService { public postAsync(url: string, content?: TContent): Observable { let headers = new HttpHeaders() + .append('Access-Control-Allow-Origin', '*') .append('accept', 'application/json') .append('Content-Type', 'application/json; charset=utf-8'); - const token = sessionStorage.getItem('github:access_token'); + let withCreds = false; if (token) { headers = headers.append('X-ACCESS-TOKEN', token); + withCreds = true; } return (content - ? this.http.post(url, content, { headers }) - : this.http.post(url, { headers })) + ? this.http.post(url, content, { headers, withCredentials:withCreds }) + : this.http.post(url, { headers, withCredentials: withCreds })) .pipe( map(result => result), timeout(this.timeout), From 9c993ce4430dcafcc402f26ed269430f191d49c2 Mon Sep 17 00:00:00 2001 From: Mathieu Guindon Date: Tue, 11 Feb 2025 02:28:04 -0500 Subject: [PATCH 4/4] implement indenter settings --- rubberduckvba.Server/Program.cs | 54 +-- rubberduckvba.client/src/app/app.module.ts | 1 + .../example-box/example-box.component.html | 6 +- .../src/app/model/indenter.model.ts | 70 +-- .../routes/indenter/indenter.component.html | 436 ++++++++++++++++-- .../app/routes/indenter/indenter.component.ts | 18 + 6 files changed, 469 insertions(+), 116 deletions(-) diff --git a/rubberduckvba.Server/Program.cs b/rubberduckvba.Server/Program.cs index b4eeace..3bc58fb 100644 --- a/rubberduckvba.Server/Program.cs +++ b/rubberduckvba.Server/Program.cs @@ -50,46 +50,26 @@ public static void Main(string[] args) policy .SetIsOriginAllowed(origin => true) .AllowAnyHeader() - .AllowAnyMethod() + .WithMethods("OPTIONS", "GET", "POST") .AllowCredentials() .Build(); }); - /* - var guestPolicy = new CorsPolicyBuilder() - .SetIsOriginAllowed(origin => true) - .WithHeaders("Content-Type", "Accept") - .WithExposedHeaders("X-ACCESS-TOKEN") - .WithMethods("GET", "POST", "OPTIONS") - .DisallowCredentials() - .SetPreflightMaxAge(TimeSpan.FromHours(48)) - .Build(); - //builder.AddDefaultPolicy(guestPolicy); - //builder.DefaultPolicyName = nameof(guestPolicy); - builder.AddPolicy(nameof(guestPolicy), guestPolicy); - - var webhookPolicy = new CorsPolicyBuilder() - #if DEBUG - .SetIsOriginAllowed(origin => true) - #else - .SetIsOriginAllowedToAllowWildcardSubdomains() - .WithOrigins("*.github.com") - #endif - .WithHeaders("Content-Type", "X-GitHub-Event", "X-GitHub-Delivery", "X-GitHub-Hook-ID", "X-Hub-Signature", "X-Hub-Signature256") - .WithMethods("POST") - .DisallowCredentials() - .SetPreflightMaxAge(TimeSpan.FromHours(48)) - .Build(); - builder.AddPolicy(nameof(webhookPolicy), webhookPolicy); - - var adminPolicy = new CorsPolicyBuilder() - .SetIsOriginAllowed(origin => true) - .WithHeaders("Content-Type", "Authorization") - .WithExposedHeaders("X-ACCESS-TOKEN") - .WithMethods("GET", "POST") - .SetPreflightMaxAge(TimeSpan.FromHours(48)) - .Build(); - builder.AddPolicy(nameof(adminPolicy), adminPolicy); - */ + + builder.AddPolicy("webhookPolicy", policy => + { + policy +#if DEBUG + .SetIsOriginAllowed(origin => true) +#else + .SetIsOriginAllowedToAllowWildcardSubdomains() + .WithOrigins("*.github.com") +#endif + .WithHeaders("Content-Type", "X-GitHub-Event", "X-GitHub-Delivery", "X-GitHub-Hook-ID", "X-Hub-Signature", "X-Hub-Signature256") + .WithMethods("POST") + .DisallowCredentials() + .SetPreflightMaxAge(TimeSpan.FromHours(48)) + .Build(); + }); }); builder.Services.AddAuthentication(options => diff --git a/rubberduckvba.client/src/app/app.module.ts b/rubberduckvba.client/src/app/app.module.ts index 1c412ae..1c4f01b 100644 --- a/rubberduckvba.client/src/app/app.module.ts +++ b/rubberduckvba.client/src/app/app.module.ts @@ -111,4 +111,5 @@ export class LowerCaseUrlSerializer extends DefaultUrlSerializer { } ] }) + export class AppModule { } diff --git a/rubberduckvba.client/src/app/components/example-box/example-box.component.html b/rubberduckvba.client/src/app/components/example-box/example-box.component.html index 02d004d..d4a189c 100644 --- a/rubberduckvba.client/src/app/components/example-box/example-box.component.html +++ b/rubberduckvba.client/src/app/components/example-box/example-box.component.html @@ -1,7 +1,7 @@
    -