Skip to content

Commit 347685b

Browse files
committed
tweaks
1 parent f2a30b0 commit 347685b

23 files changed

+451
-108
lines changed

rubberduckvba.Server/Api/Admin/AdminController.cs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.Extensions.Options;
55
using rubberduckvba.Server.Model.Entity;
66
using rubberduckvba.Server.Services;
7+
using System.Security.Principal;
78

89
namespace rubberduckvba.Server.Api.Admin;
910

@@ -44,7 +45,7 @@ public IActionResult ClearCache()
4445
}
4546

4647
[Authorize("github")]
47-
[HttpGet("admin/audits")]
48+
[HttpGet("admin/audits/pending")]
4849
public async Task<IActionResult> GetPendingAudits()
4950
{
5051
var edits = await audits.GetPendingItems<FeatureEditEntity>();
@@ -53,6 +54,88 @@ public async Task<IActionResult> GetPendingAudits()
5354
return Ok(new { edits = edits.ToArray(), other = ops.ToArray() });
5455
}
5556

57+
[Authorize("github")]
58+
[HttpGet("admin/audits/{featureId}")]
59+
public async Task<IActionResult> GetPendingAudits([FromRoute] int featureId)
60+
{
61+
var edits = await audits.GetPendingItems<FeatureEditEntity>(featureId);
62+
var ops = await audits.GetPendingItems<FeatureOpEntity>(featureId);
63+
64+
return Ok(new { edits = edits.ToArray(), other = ops.ToArray() });
65+
}
66+
67+
[Authorize("github")]
68+
[HttpPost("admin/audits/approve/{id}")]
69+
public async Task<IActionResult> ApprovePendingAudit([FromRoute] int id)
70+
{
71+
if (User.Identity is not IIdentity identity)
72+
{
73+
// this is arguably a bug in the authentication middleware, but we can handle it gracefully here
74+
return Unauthorized("User identity is not available.");
75+
}
76+
77+
var edits = await audits.GetPendingItems<FeatureEditEntity>();
78+
AuditEntity? audit;
79+
80+
audit = edits.SingleOrDefault(e => e.Id == id);
81+
if (audit is null)
82+
{
83+
var ops = await audits.GetPendingItems<FeatureOpEntity>();
84+
audit = ops.SingleOrDefault(e => e.Id == id);
85+
}
86+
87+
if (audit is null)
88+
{
89+
// TODO log this
90+
return BadRequest("Invalid ID");
91+
}
92+
93+
if (!audit.IsPending)
94+
{
95+
// TODO log this
96+
return BadRequest($"This operation has already been audited");
97+
}
98+
99+
await audits.Approve(audit, identity);
100+
return Ok("Operation was approved successfully.");
101+
}
102+
103+
[Authorize("github")]
104+
[HttpPost("admin/audits/reject/{id}")]
105+
public async Task<IActionResult> RejectPendingAudit([FromRoute] int id)
106+
{
107+
if (User.Identity is not IIdentity identity)
108+
{
109+
// this is arguably a bug in the authentication middleware, but we can handle it gracefully here
110+
return Unauthorized("User identity is not available.");
111+
}
112+
113+
var edits = await audits.GetPendingItems<FeatureEditEntity>();
114+
AuditEntity? audit;
115+
116+
audit = edits.SingleOrDefault(e => e.Id == id);
117+
if (audit is null)
118+
{
119+
var ops = await audits.GetPendingItems<FeatureOpEntity>();
120+
audit = ops.SingleOrDefault(e => e.Id == id);
121+
}
122+
123+
if (audit is null)
124+
{
125+
// TODO log this
126+
return BadRequest("Invalid ID");
127+
}
128+
129+
if (!audit.IsPending)
130+
{
131+
// TODO log this
132+
return BadRequest($"This operation has already been audited");
133+
}
134+
135+
await audits.Reject(audit, identity);
136+
return Ok("Operation was rejected successfully.");
137+
}
138+
56139
#if DEBUG
57140
[AllowAnonymous]
58141
[HttpGet("admin/config/current")]

rubberduckvba.Server/Api/Features/FeatureEditViewModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public Feature ToFeature()
2929
return new Feature
3030
{
3131
Id = Id ?? default,
32-
ParentId = FeatureId,
32+
FeatureId = FeatureId,
3333
RepositoryId = RepositoryId,
3434
Name = Name,
3535
Title = Title,
@@ -43,7 +43,7 @@ public Feature ToFeature()
4343
public FeatureEditViewModel(Feature model, FeatureOptionViewModel[] features, RepositoryOptionViewModel[] repositories)
4444
{
4545
Id = model.Id;
46-
FeatureId = model.ParentId;
46+
FeatureId = model.FeatureId;
4747
RepositoryId = model.RepositoryId;
4848

4949
Name = model.Name;

rubberduckvba.Server/Api/Features/FeatureViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public FeatureViewModel(Feature model, bool summaryOnly = false)
1111
DateInserted = model.DateTimeInserted;
1212
DateUpdated = model.DateTimeUpdated;
1313

14-
FeatureId = model.ParentId;
14+
FeatureId = model.FeatureId;
1515
FeatureName = model.FeatureName;
1616

1717
Name = model.Name;

rubberduckvba.Server/Api/Features/FeaturesController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ public async Task<ActionResult<FeatureEditViewModel>> Update([FromBody] FeatureE
218218

219219
[HttpPost("features/delete")]
220220
[Authorize("github")]
221-
public async Task Delete([FromBody] IFeature model)
221+
public async Task Delete([FromBody] Feature model)
222222
{
223223
if (model.Id == default)
224224
{

rubberduckvba.Server/Data/FeaturesRepository.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public FeaturesRepository(IOptions<ConnectionSettings> settings)
1717
a.[DateTimeInserted],
1818
a.[DateTimeUpdated],
1919
a.[RepositoryId],
20-
a.[ParentId] AS [FeatureId],
20+
a.[ParentId],
2121
f.[Name] AS [FeatureName],
2222
a.[Name],
2323
a.[Title],

rubberduckvba.Server/Model/Entity/FeatureEntity.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ public record class AuditEntity
2626
public DateTime DateInserted { get; init; }
2727
public DateTime? DateModified { get; init; }
2828
public string Author { get; init; } = string.Empty;
29+
30+
public string? ApprovedBy { get; init; }
31+
public DateTime? ApprovedAt { get; init; }
32+
public string? RejectedBy { get; init; }
33+
public DateTime? RejectedAt { get; init; }
34+
35+
public bool IsPending => !ApprovedAt.HasValue && !RejectedAt.HasValue;
36+
public bool IsApproved => ApprovedAt.HasValue && !RejectedAt.HasValue;
2937
}
3038

3139
public record class FeatureEditEntity : AuditEntity
@@ -34,10 +42,6 @@ public record class FeatureEditEntity : AuditEntity
3442
public string FieldName { get; init; } = string.Empty;
3543
public string? ValueBefore { get; init; }
3644
public string ValueAfter { get; init; } = string.Empty;
37-
public string? ApprovedBy { get; init; }
38-
public DateTime? ApprovedAt { get; init; }
39-
public string? RejectedBy { get; init; }
40-
public DateTime? RejectedAt { get; init; }
4145
}
4246

4347
public enum FeatureOperation
@@ -52,7 +56,6 @@ public record class FeatureOpEntity : AuditEntity
5256

5357
public int? ParentId { get; init; }
5458
public string FeatureName { get; init; } = default!;
55-
public int RepositoryId { get; init; }
5659
public string Title { get; init; } = default!;
5760
public string ShortDescription { get; init; } = default!;
5861
public string Description { get; init; } = default!;

rubberduckvba.Server/Model/Feature.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public Feature(FeatureEntity entity) : this()
2727
DateTimeInserted = entity.DateTimeInserted;
2828
DateTimeUpdated = entity.DateTimeUpdated;
2929
Name = entity.Name;
30-
ParentId = entity.ParentId;
30+
FeatureId = entity.ParentId;
3131
FeatureName = entity.FeatureName;
3232
RepositoryId = (Services.RepositoryId)entity.RepositoryId;
3333
Title = entity.Title;
@@ -45,7 +45,7 @@ public Feature(FeatureEntity entity) : this()
4545
public DateTime? DateTimeUpdated { get; init; }
4646
public string Name { get; init; } = string.Empty;
4747

48-
public int? ParentId { get; init; }
48+
public int? FeatureId { get; init; }
4949
public string FeatureName { get; init; } = string.Empty;
5050
public Services.RepositoryId RepositoryId { get; init; } = Services.RepositoryId.Rubberduck;
5151
public string Title { get; init; } = string.Empty;
@@ -69,7 +69,7 @@ public Feature(FeatureEntity entity) : this()
6969
IsNew = IsNew,
7070
Name = Name,
7171
ShortDescription = ShortDescription,
72-
ParentId = ParentId,
72+
ParentId = FeatureId,
7373
FeatureName = FeatureName,
7474
RepositoryId = (int)Services.RepositoryId.Rubberduck,
7575
Title = Title,

rubberduckvba.Server/Services/RubberduckDbService.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public interface IAuditService
6767
Task UpdateFeature(Feature feature, IIdentity identity);
6868

6969

70-
Task<IEnumerable<T>> GetPendingItems<T>() where T : AuditEntity;
70+
Task<IEnumerable<T>> GetPendingItems<T>(int? featureId = default) where T : AuditEntity;
7171

7272
Task Approve<T>(T entity, IIdentity identity) where T : AuditEntity;
7373
Task Reject<T>(T entity, IIdentity identity) where T : AuditEntity;
@@ -107,8 +107,8 @@ public async Task Reject<T>(T entity, IIdentity identity) where T : AuditEntity
107107
{
108108
var procName = entity switch
109109
{
110-
FeatureOpEntity => "audits.ApproveFeatureOp",
111-
FeatureEditEntity => "audits.ApproveFeatureEdit",
110+
FeatureOpEntity => "audits.RejectFeatureOp",
111+
FeatureEditEntity => "audits.RejectFeatureEdit",
112112
_ => throw new NotSupportedException($"The entity type {typeof(T).Name} is not supported for approval."),
113113
};
114114
await ApproveOrReject(procName, entity.Id, identity);
@@ -149,7 +149,7 @@ private async Task SubmitFeatureOp(Feature feature, IIdentity identity, FeatureO
149149
login,
150150
name = feature.Name,
151151
action = Convert.ToInt32(operation),
152-
parentId = feature.ParentId,
152+
parentId = feature.FeatureId,
153153
title = feature.Title,
154154
summary = feature.ShortDescription,
155155
description = feature.Description,
@@ -203,16 +203,29 @@ private async Task SubmitFeatureEdit(Feature feature, IIdentity identity)
203203
});
204204
}
205205

206-
public async Task<IEnumerable<T>> GetPendingItems<T>() where T : AuditEntity
206+
public async Task<IEnumerable<T>> GetPendingItems<T>(int? featureId = default) where T : AuditEntity
207207
{
208208
using var db = await GetDbConnection();
209-
var tableName = typeof(T).Name switch
209+
var (tableName, columns) = typeof(T).Name switch
210210
{
211-
nameof(FeatureOpEntity) => "audits.FeatureOps",
212-
nameof(FeatureEditEntity) => "audits.FeatureEdits",
211+
nameof(FeatureOpEntity) => ("audits.FeatureOps src", string.Join(',', typeof(FeatureOpEntity).GetProperties().Where(p => p.CanWrite).Select(p => $"src.[{p.Name}]"))),
212+
nameof(FeatureEditEntity) => ("audits.FeatureEdits src", string.Join(',', typeof(FeatureEditEntity).GetProperties().Where(p => p.CanWrite).Select(p => $"src.[{p.Name}]"))),
213213
_ => throw new NotSupportedException($"The entity type {typeof(T).Name} is not supported for pending items retrieval."),
214214
};
215-
return await db.QueryAsync<T>($"SELECT * FROM {tableName} WHERE ApprovedBy IS NULL AND RejectedBy IS NULL ORDER BY DateInserted DESC");
215+
216+
const string pendingFilter = "src.[ApprovedBy] IS NULL AND src.[RejectedBy] IS NULL";
217+
218+
var sql = featureId.HasValue
219+
? typeof(T).Name switch
220+
{
221+
nameof(FeatureOpEntity) => $"SELECT {columns} FROM {tableName} INNER JOIN dbo.Features f ON src.[FeatureName] = f.[Name] WHERE {pendingFilter} AND f.[Id] = {featureId}",
222+
nameof(FeatureEditEntity) => $"SELECT {columns} FROM {tableName} WHERE {pendingFilter} AND src.[FeatureId] = {featureId}",
223+
_ => throw new NotSupportedException($"The entity type {typeof(T).Name} is not supported for pending items retrieval."),
224+
}
225+
: $"SELECT {columns} FROM {tableName} WHERE {pendingFilter}";
226+
227+
sql += " ORDER BY src.[DateInserted] DESC";
228+
return await db.QueryAsync<T>(sql);
216229
}
217230
}
218231

@@ -257,7 +270,7 @@ public async Task<FeatureGraph> ResolveFeature(RepositoryId repositoryId, string
257270
{
258271
var features = _featureServices.Get(topLevelOnly: false).ToList();
259272
var feature = features.Single(e => string.Equals(e.Name, name, StringComparison.OrdinalIgnoreCase));
260-
var children = features.Where(e => e.ParentId == feature.Id);
273+
var children = features.Where(e => e.FeatureId == feature.Id);
261274
return new FeatureGraph(feature.ToEntity())
262275
{
263276
Features = children.ToArray()

rubberduckvba.Server/Services/rubberduckdb/FeatureServices.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ public class FeatureServices(
1414
public int? GetId(string name) => featureRepository.TryGetId(name, out var id) ? id : null;
1515
public IEnumerable<Feature> Get(bool topLevelOnly = true)
1616
{
17-
return featureRepository.GetAll()
18-
.Where(e => !topLevelOnly || e.ParentId is null)
19-
.Select(e => new Feature(e));
17+
var features = featureRepository.GetAll();
18+
return features
19+
.Where(e => e.ParentId == null || !topLevelOnly)
20+
.Select(e => new Feature(e))
21+
.ToList();
2022
}
2123

2224
public Inspection GetInspection(string name)

rubberduckvba.client/src/app/components/auth-menu/auth-menu.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class AuthMenuComponent implements OnInit {
3939

4040
ngOnInit(): void {
4141
this.getUserInfo();
42-
this.api.getPendingAudits().subscribe(e => this.pendingAudits = e.edits.length + e.other.length);
42+
this.api.getAllPendingAudits().subscribe(e => {if (e) { this.pendingAudits = e.edits.length + e.other.length }});
4343
}
4444

4545
getUserInfo(): void {

0 commit comments

Comments
 (0)