Skip to content

Commit 3e70aff

Browse files
authored
Merge pull request #61 from rubberduck-vba/webhook
Webhook + Indenter
2 parents 57b80c2 + 9c993ce commit 3e70aff

22 files changed

+943
-196
lines changed

rubberduckvba.Server/Api/Admin/WebhookController.cs

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,35 @@ public WebhookController(
2222

2323
[Authorize("webhook")]
2424
[HttpPost("webhook/github")]
25-
public async Task<IActionResult> GitHub([FromBody] dynamic body)
26-
{
27-
//var reader = new StreamReader(Request.Body);
28-
//var json = await reader.ReadToEndAsync();
29-
string json = body.ToString();
30-
31-
var payload = JsonSerializer.Deserialize<PushWebhookPayload>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
32-
?? throw new InvalidOperationException("Could not deserialize payload");
33-
var eventType = _validator.Validate(payload, json, Request.Headers, out var content, out var gitref);
34-
35-
if (eventType == WebhookPayloadType.Push)
36-
{
37-
var jobId = _hangfire.UpdateXmldocContent();
38-
var message = $"Webhook push event was accepted. Tag '{gitref?.Name}' associated to the payload will be processed by JobId '{jobId}'.";
39-
40-
Logger.LogInformation(message);
41-
return Ok(message);
42-
}
43-
else if (eventType == WebhookPayloadType.Ping)
44-
{
45-
Logger.LogInformation("Webhook ping event was accepted; nothing to process.");
46-
return Ok();
47-
}
48-
else if (eventType == WebhookPayloadType.Greeting)
25+
public async Task<IActionResult> GitHub([FromBody] dynamic body) =>
26+
GuardInternalAction(() =>
4927
{
50-
Logger.LogInformation("Webhook push event was accepted; nothing to process. {content}", content);
51-
return string.IsNullOrWhiteSpace(content) ? NoContent() : Ok(content);
52-
}
53-
54-
// reject the payload
55-
return BadRequest();
56-
}
28+
string json = body.ToString();
29+
30+
var payload = JsonSerializer.Deserialize<PushWebhookPayload>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
31+
?? throw new InvalidOperationException("Could not deserialize payload");
32+
var eventType = _validator.Validate(payload, json, Request.Headers, out var content, out var gitref);
33+
34+
if (eventType == WebhookPayloadType.Push)
35+
{
36+
var jobId = _hangfire.UpdateXmldocContent();
37+
var message = $"Webhook push event was accepted. Tag '{gitref?.Name}' associated to the payload will be processed by JobId '{jobId}'.";
38+
39+
Logger.LogInformation(message);
40+
return Ok(message);
41+
}
42+
else if (eventType == WebhookPayloadType.Ping)
43+
{
44+
Logger.LogInformation("Webhook ping event was accepted; nothing to process.");
45+
return Ok();
46+
}
47+
else if (eventType == WebhookPayloadType.Greeting)
48+
{
49+
Logger.LogInformation("Webhook push event was accepted; nothing to process. {content}", content);
50+
return string.IsNullOrWhiteSpace(content) ? NoContent() : Ok(content);
51+
}
52+
53+
// reject the payload
54+
return BadRequest();
55+
});
5756
}

rubberduckvba.Server/Api/Auth/AuthController.cs

Lines changed: 91 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.AspNetCore.Mvc;
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.Mvc;
23
using Microsoft.Extensions.Options;
34
using Octokit;
45
using Octokit.Internal;
@@ -23,91 +24,113 @@ public record class SignInViewModel
2324
}
2425

2526
[ApiController]
26-
public class AuthController(IOptions<GitHubSettings> configuration, IOptions<ApiSettings> api, ILogger<AuthController> logger) : ControllerBase
27+
[AllowAnonymous]
28+
public class AuthController : RubberduckApiController
2729
{
30+
private readonly IOptions<GitHubSettings> configuration;
31+
32+
public AuthController(IOptions<GitHubSettings> configuration, IOptions<ApiSettings> api, ILogger<AuthController> logger)
33+
: base(logger)
34+
{
35+
this.configuration = configuration;
36+
}
37+
2838
[HttpGet("auth")]
39+
[AllowAnonymous]
2940
public IActionResult Index()
3041
{
31-
var claims = HttpContext.User.Claims.ToDictionary(claim => claim.Type, claim => claim.Value);
32-
var hasName = claims.TryGetValue(ClaimTypes.Name, out var name);
33-
var hasRole = claims.TryGetValue(ClaimTypes.Role, out var role);
34-
35-
if (hasName && hasRole)
42+
return GuardInternalAction(() =>
3643
{
37-
if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(role))
44+
var claims = HttpContext.User.Claims.ToDictionary(claim => claim.Type, claim => claim.Value);
45+
var hasName = claims.TryGetValue(ClaimTypes.Name, out var name);
46+
var hasRole = claims.TryGetValue(ClaimTypes.Role, out var role);
47+
48+
if (hasName && hasRole)
3849
{
39-
return BadRequest();
50+
if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(role))
51+
{
52+
return BadRequest();
53+
}
54+
55+
var isAuthenticated = HttpContext.User.Identity?.IsAuthenticated ?? false;
56+
var model = new UserViewModel
57+
{
58+
Name = name,
59+
IsAuthenticated = isAuthenticated,
60+
IsAdmin = role == configuration.Value.OwnerOrg
61+
};
62+
63+
return Ok(model);
4064
}
41-
42-
var isAuthenticated = HttpContext.User.Identity?.IsAuthenticated ?? false;
43-
var model = new UserViewModel
65+
else
4466
{
45-
Name = name,
46-
IsAuthenticated = isAuthenticated,
47-
IsAdmin = role == configuration.Value.OwnerOrg
48-
};
49-
50-
return Ok(model);
51-
}
52-
else
53-
{
54-
return Ok(UserViewModel.Anonymous);
55-
}
67+
return Ok(UserViewModel.Anonymous);
68+
}
69+
});
5670
}
5771

5872
[HttpPost("auth/signin")]
73+
[AllowAnonymous]
5974
public IActionResult SessionSignIn(SignInViewModel vm)
6075
{
61-
if (User.Identity?.IsAuthenticated ?? false)
76+
return GuardInternalAction(() =>
6277
{
63-
logger.LogInformation("Signin was requested, but user is already authenticated. Redirecting to home page...");
64-
return Redirect("/");
65-
}
78+
if (User.Identity?.IsAuthenticated ?? false)
79+
{
80+
Logger.LogInformation("Signin was requested, but user is already authenticated. Redirecting to home page...");
81+
return Redirect("/");
82+
}
6683

67-
var clientId = configuration.Value.ClientId;
68-
var agent = configuration.Value.UserAgent;
84+
var clientId = configuration.Value.ClientId;
85+
var agent = configuration.Value.UserAgent;
6986

70-
var github = new GitHubClient(new ProductHeaderValue(agent));
71-
var request = new OauthLoginRequest(clientId)
72-
{
73-
AllowSignup = false,
74-
Scopes = { "read:user", "read:org" },
75-
State = vm.State
76-
};
77-
78-
logger.LogInformation("Requesting OAuth app GitHub login url...");
79-
var url = github.Oauth.GetGitHubLoginUrl(request);
80-
if (url is null)
81-
{
82-
logger.LogInformation("OAuth login was cancelled by the user or did not return a url.");
83-
return Forbid();
84-
}
87+
var github = new GitHubClient(new ProductHeaderValue(agent));
88+
var request = new OauthLoginRequest(clientId)
89+
{
90+
AllowSignup = false,
91+
Scopes = { "read:user", "read:org" },
92+
State = vm.State
93+
};
8594

86-
logger.LogInformation("Returning the login url for the client to redirect. State: {xsrf}", vm.State);
87-
return Ok(url.ToString());
95+
Logger.LogInformation("Requesting OAuth app GitHub login url...");
96+
var url = github.Oauth.GetGitHubLoginUrl(request);
97+
if (url is null)
98+
{
99+
Logger.LogInformation("OAuth login was cancelled by the user or did not return a url.");
100+
return Forbid();
101+
}
102+
103+
Logger.LogInformation("Returning the login url for the client to redirect. State: {xsrf}", vm.State);
104+
return Ok(url.ToString());
105+
});
88106
}
89107

90108
[HttpPost("auth/github")]
91-
public async Task<IActionResult> OnGitHubCallback(SignInViewModel vm)
109+
[AllowAnonymous]
110+
public IActionResult OnGitHubCallback(SignInViewModel vm)
92111
{
93-
logger.LogInformation("OAuth token was received. State: {state}", vm.State);
94-
var clientId = configuration.Value.ClientId;
95-
var clientSecret = configuration.Value.ClientSecret;
96-
var agent = configuration.Value.UserAgent;
112+
return GuardInternalAction(() =>
113+
{
114+
Logger.LogInformation("OAuth token was received. State: {state}", vm.State);
115+
var clientId = configuration.Value.ClientId;
116+
var clientSecret = configuration.Value.ClientSecret;
117+
var agent = configuration.Value.UserAgent;
97118

98-
var github = new GitHubClient(new ProductHeaderValue(agent));
119+
var github = new GitHubClient(new ProductHeaderValue(agent));
99120

100-
var request = new OauthTokenRequest(clientId, clientSecret, vm.Code);
101-
var token = await github.Oauth.CreateAccessToken(request);
102-
if (token is null)
103-
{
104-
logger.LogWarning("OAuth access token was not created.");
105-
return Unauthorized();
106-
}
121+
var request = new OauthTokenRequest(clientId, clientSecret, vm.Code);
122+
var token = github.Oauth.CreateAccessToken(request).GetAwaiter().GetResult();
123+
if (token is null)
124+
{
125+
Logger.LogWarning("OAuth access token was not created.");
126+
return Unauthorized();
127+
}
128+
129+
Logger.LogInformation("OAuth access token was created. Authorizing...");
130+
var authorizedToken = AuthorizeAsync(token.AccessToken).GetAwaiter().GetResult();
107131

108-
logger.LogInformation("OAuth access token was created. Authorizing...");
109-
var authorizedToken = await AuthorizeAsync(token.AccessToken);
110-
return authorizedToken is null ? Unauthorized() : Ok(vm with { Token = authorizedToken });
132+
return authorizedToken is null ? Unauthorized() : Ok(vm with { Token = authorizedToken });
133+
});
111134
}
112135

113136
private async Task<string?> AuthorizeAsync(string token)
@@ -119,13 +142,13 @@ public async Task<IActionResult> OnGitHubCallback(SignInViewModel vm)
119142
var githubUser = await github.User.Current();
120143
if (githubUser.Suspended)
121144
{
122-
logger.LogWarning("User {name} with login '{login}' ({url}) is a suspended GitHub account and will not be authorized.", githubUser.Name, githubUser.Login, githubUser.Url);
145+
Logger.LogWarning("User login '{login}' ({name}) is a suspended GitHub account and will not be authorized.", githubUser.Login, githubUser.Name);
123146
return default;
124147
}
125148

126149
var identity = new ClaimsIdentity("github", ClaimTypes.Name, ClaimTypes.Role);
127150
identity.AddClaim(new Claim(ClaimTypes.Name, githubUser.Login));
128-
logger.LogInformation("Creating claims identity for GitHub login '{login}'...", githubUser.Login);
151+
Logger.LogInformation("Creating claims identity for GitHub login '{login}'...", githubUser.Login);
129152

130153
var orgs = await github.Organization.GetAllForUser(githubUser.Login);
131154
var rdOrg = orgs.SingleOrDefault(org => org.Id == configuration.Value.RubberduckOrgId);
@@ -135,27 +158,27 @@ public async Task<IActionResult> OnGitHubCallback(SignInViewModel vm)
135158
identity.AddClaim(new Claim(ClaimTypes.Role, configuration.Value.OwnerOrg));
136159
identity.AddClaim(new Claim(ClaimTypes.Authentication, token));
137160
identity.AddClaim(new Claim("access_token", token));
138-
logger.LogDebug("GitHub Organization claims were granted. Creating claims principal...");
161+
Logger.LogDebug("GitHub Organization claims were granted. Creating claims principal...");
139162

140163
var principal = new ClaimsPrincipal(identity);
141164
var roles = string.Join(",", identity.Claims.Where(claim => claim.Type == ClaimTypes.Role).Select(claim => claim.Value));
142165

143166
HttpContext.User = principal;
144167
Thread.CurrentPrincipal = HttpContext.User;
145168

146-
logger.LogInformation("GitHub user with login {login} has signed in with role authorizations '{role}'.", githubUser.Login, configuration.Value.OwnerOrg);
169+
Logger.LogInformation("GitHub user with login {login} has signed in with role authorizations '{role}'.", githubUser.Login, configuration.Value.OwnerOrg);
147170
return token;
148171
}
149172
else
150173
{
151-
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);
174+
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);
152175
return default;
153176
}
154177
}
155178
catch (Exception)
156179
{
157180
// just ignore: configuration needs the org (prod) client app id to avoid throwing this exception
158-
logger.LogWarning("An exception was thrown. Verify GitHub:ClientId and GitHub:ClientSecret configuration; authorization fails.");
181+
Logger.LogWarning("An exception was thrown. Verify GitHub:ClientId and GitHub:ClientSecret configuration; authorization fails.");
159182
return default;
160183
}
161184
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.Mvc;
3+
using RubberduckServices;
4+
5+
namespace rubberduckvba.Server.Api.Indenter;
6+
7+
[AllowAnonymous]
8+
public class IndenterController : RubberduckApiController
9+
{
10+
private readonly IIndenterService service;
11+
12+
public IndenterController(IIndenterService service, ILogger<IndenterController> logger)
13+
: base(logger)
14+
{
15+
this.service = service;
16+
}
17+
18+
[HttpGet("indenter/version")]
19+
[AllowAnonymous]
20+
public IActionResult Version() =>
21+
GuardInternalAction(() => Ok(service.IndenterVersion()));
22+
23+
[HttpGet("indenter/defaults")]
24+
[AllowAnonymous]
25+
public IActionResult DefaultSettings() =>
26+
GuardInternalAction(() =>
27+
{
28+
var result = new IndenterViewModel
29+
{
30+
IndenterVersion = service.IndenterVersion(),
31+
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",
32+
AlignCommentsWithCode = true,
33+
EmptyLineHandlingMethod = IndenterEmptyLineHandling.Indent,
34+
ForceCompilerDirectivesInColumn1 = true,
35+
GroupRelatedProperties = false,
36+
IndentSpaces = 4,
37+
IndentCase = true,
38+
IndentEntireProcedureBody = true,
39+
IndentEnumTypeAsProcedure = true,
40+
VerticallySpaceProcedures = true,
41+
LinesBetweenProcedures = 1,
42+
IndentFirstCommentBlock = true,
43+
IndentFirstDeclarationBlock = true,
44+
EndOfLineCommentStyle = IndenterEndOfLineCommentStyle.SameGap,
45+
};
46+
47+
return Ok(result);
48+
});
49+
50+
[HttpPost("indenter/indent")]
51+
[AllowAnonymous]
52+
public IActionResult Indent(IndenterViewModel model) =>
53+
GuardInternalAction(() =>
54+
{
55+
if (model is null)
56+
{
57+
throw new ArgumentNullException(nameof(model));
58+
}
59+
60+
var result = service.IndentAsync(model).GetAwaiter().GetResult();
61+
return Ok(result);
62+
});
63+
}

0 commit comments

Comments
 (0)