Skip to content
Open
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
24 changes: 12 additions & 12 deletions AdSpot.Api/AdSpot.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="AppAny.HotChocolate.FluentValidation" Version="0.11.1" />
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="AppAny.HotChocolate.FluentValidation" Version="0.11.2" />
<PackageReference Include="FluentValidation" Version="11.10.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="HotChocolate.AspNetCore" Version="13.9.0" />
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="13.9.0" />
<PackageReference Include="HotChocolate.Data" Version="13.9.0" />
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="13.9.0" />
<PackageReference Include="HotChocolate.PersistedQueries.FileSystem" Version="13.9.0" />
<PackageReference Include="HotChocolate.Types.Analyzers" Version="13.9.0" />
<PackageReference Include="HotChocolate.AspNetCore" Version="14.1.0" />
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="14.1.0" />
<PackageReference Include="HotChocolate.Data" Version="14.1.0" />
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="14.1.0" />
<PackageReference Include="HotChocolate.PersistedOperations.FileSystem" Version="14.1.0" />
<PackageReference Include="HotChocolate.Types.Analyzers" Version="14.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.1" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.4.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 2 additions & 1 deletion AdSpot.Api/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
global using AdSpot.Api.Mutations.Errors;
global using AdSpot.Api.Mutations.Inputs;
global using AdSpot.Api.Mutations.Payloads;
global using AdSpot.Api.Policies;
global using AdSpot.Api.Repositories;
global using AdSpot.Api.Services;
global using AdSpot.Api.Subscriptions;
Expand All @@ -24,8 +25,8 @@
global using HotChocolate.Data.Filters.Expressions;
global using HotChocolate.Execution;
global using HotChocolate.Language;
global using HotChocolate.Resolvers;
global using HotChocolate.Subscriptions;
global using Microsoft.AspNetCore.Authentication;
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.AspNetCore.Http;
global using Microsoft.EntityFrameworkCore;
Expand Down
4 changes: 2 additions & 2 deletions AdSpot.Api/Mutations/ConnectionMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public class ConnectionMutations
{
[Authorize]
[Error<ConnectionAlreadyExistsError>]
public MutationResult<Connection> AddConnection(
public FieldResult<Connection> AddConnection(
int userId,
int platformId,
string accountHandle,
Expand Down Expand Up @@ -33,7 +33,7 @@ ConnectionRepository repo
}

[Error<InstagramOauthError>]
public async Task<MutationResult<Connection?>> ExchangeInstagramAuthCodeForToken(
public async Task<FieldResult<Connection?>> ExchangeInstagramAuthCodeForToken(
int userId,
int platformId,
string authCode,
Expand Down
6 changes: 3 additions & 3 deletions AdSpot.Api/Mutations/FlairMutations.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
namespace AdSpot.Api.Mutations;
namespace AdSpot.Api.Mutations;

[MutationType]
public class FlairMutations
{
[Error<FlairAlreadyExistsError>]
public MutationResult<IQueryable<Flair>> AddFlair(int userId, string flairTitle, FlairRepository flairRepo)
public FieldResult<IQueryable<Flair>> AddFlair(int userId, string flairTitle, FlairRepository flairRepo)
{
var flairExists = flairRepo.GetFlair(userId, flairTitle).Any();
if (flairExists)
Expand All @@ -17,7 +17,7 @@ public MutationResult<IQueryable<Flair>> AddFlair(int userId, string flairTitle,
return new(flair);
}

public MutationResult<IQueryable<Flair>> DeleteFlair(int userId, string flairTitle, FlairRepository flairRepo)
public FieldResult<IQueryable<Flair>> DeleteFlair(int userId, string flairTitle, FlairRepository flairRepo)
{
var flairs = flairRepo.GetFlair(userId, flairTitle);
if (flairs.Any())
Expand Down
4 changes: 2 additions & 2 deletions AdSpot.Api/Mutations/ListingMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class ListingMutations
//[UseProjection]
[Error<InvalidListingTypeIdError>]
[Error<AccountHasNotBeenConnectedError>]
public async Task<MutationResult<Listing>> AddListing(
public async Task<FieldResult<Listing>> AddListing(
int listingTypeId,
int userId,
decimal price,
Expand Down Expand Up @@ -50,7 +50,7 @@ [Service] ITopicEventSender topicEventSender
[Error<InvalidPriceError>]
[Error<InvalidListingIdError>]
[Error<ListingDoesNotBelongToUserError>]
public MutationResult<Listing> UpdateListingPrice(
public FieldResult<Listing> UpdateListingPrice(
int listingId,
int userId,
decimal price,
Expand Down
8 changes: 4 additions & 4 deletions AdSpot.Api/Mutations/OrderMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class OrderMutations
[Error<InvalidListingIdError>]
[Error<CannotOrderOwnListingError>]
[Error<ListingPriceHasChangedError>]
public async Task<MutationResult<Order>> OrderListing(
public async Task<FieldResult<Order>> OrderListing(
int listingId,
int userId,
decimal price,
Expand Down Expand Up @@ -55,7 +55,7 @@ [Service] ITopicEventSender topicEventSender
[Authorize]
[Error<InvalidOrderIdError>]
[Error<ListingDoesNotBelongToUserError>]
public MutationResult<Order> AcceptOrder(int userId, int orderId, OrderRepository orderRepo)
public FieldResult<Order> AcceptOrder(int userId, int orderId, OrderRepository orderRepo)
{
var order = orderRepo.GetOrderById(orderId).Include(o => o.Listing).FirstOrDefault();

Expand All @@ -77,7 +77,7 @@ public MutationResult<Order> AcceptOrder(int userId, int orderId, OrderRepositor
[Authorize]
[Error<InvalidOrderIdError>]
[Error<ListingDoesNotBelongToUserError>]
public MutationResult<Order> RejectOrder(int userId, int orderId, OrderRepository orderRepo)
public FieldResult<Order> RejectOrder(int userId, int orderId, OrderRepository orderRepo)
{
var order = orderRepo.GetOrderById(orderId).Include(o => o.Listing).FirstOrDefault();

Expand All @@ -97,7 +97,7 @@ public MutationResult<Order> RejectOrder(int userId, int orderId, OrderRepositor
}

[Error<InvalidOrderIdError>]
public MutationResult<Order> SubmitDeliverable(
public FieldResult<Order> SubmitDeliverable(
[UseFluentValidation, UseValidator<SubmitDeliverableInputValidator>] SubmitDeliverableInput input,
OrderRepository orderRepo
)
Expand Down
8 changes: 4 additions & 4 deletions AdSpot.Api/Mutations/UserMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
public class UserMutations
{
[Error<AccountWithEmailAlreadyExistsError>]
public MutationResult<AddUserPayload> AddUser(
public FieldResult<AddUserPayload> AddUser(
[UseFluentValidation, UseValidator<AddUserInputValidator>] AddUserInput input,
UserRepository repo,
[Service] IOptions<JwtOptions> jwtOptions,
Expand Down Expand Up @@ -33,7 +33,7 @@ [Service] KeyManager keyManager
}

[Error<InvalidUserIdError>]
public MutationResult<User> DeleteUser(int userId, UserRepository repo)
public FieldResult<User> DeleteUser(int userId, UserRepository repo)
{
var user = repo.GetUserById(userId).FirstOrDefault();

Expand All @@ -47,7 +47,7 @@ public MutationResult<User> DeleteUser(int userId, UserRepository repo)
}

[Error<InvalidUserIdError>]
public MutationResult<User> UpdatePassword(int userId, string password, UserRepository repo)
public FieldResult<User> UpdatePassword(int userId, string password, UserRepository repo)
{
var user = repo.GetUserById(userId).FirstOrDefault();

Expand All @@ -62,7 +62,7 @@ public MutationResult<User> UpdatePassword(int userId, string password, UserRepo

[Error<UserNotFoundError>]
[Error<UserInvalidCredentialsError>]
public MutationResult<LoginPayload> Login(
public FieldResult<LoginPayload> Login(
string email,
string password,
UserRepository repo,
Expand Down
27 changes: 27 additions & 0 deletions AdSpot.Api/Policies/SelfRequirement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Authorization;

namespace AdSpot.Api.Policies;

public class SelfAuthorizationHandler : AuthorizationHandler<SelfRequirement, IResolverContext>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
SelfRequirement requirement,
IResolverContext resource
)
{
var graphqlVariableUserId = resource.Variables.GetVariable<int>("userId");
if (context.User.HasClaim(claim => claim.Type == "sub"))
{
var success = int.TryParse(context.User.FindFirstValue("sub"), out var userId);
if (success && userId == graphqlVariableUserId)
{
context.Succeed(requirement);
}
}

return Task.CompletedTask;
}
}

public class SelfRequirement : IAuthorizationRequirement { }
55 changes: 27 additions & 28 deletions AdSpot.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,37 @@
.AddScoped<AddUserInputValidator>()
.AddScoped<SubmitDeliverableInputValidator>();

builder
.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
ValidateLifetime = !isDevelopment,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(keyManager.RsaKey),
ClockSkew = TimeSpan.Zero,
};

options.MapInboundClaims = false;
});

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("self", policy => policy.AddRequirements(new SelfRequirement()));
});
builder.Services.AddSingleton<Microsoft.AspNetCore.Authorization.IAuthorizationHandler, SelfAuthorizationHandler>();

builder.Services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters();

builder
.Services.AddGraphQLServer()
.AddAuthorization()
.AddFluentValidation()
.UsePersistedQueryPipeline()
.AddReadOnlyFileSystemQueryStorage("./PersistedQueries")
.UsePersistedOperationPipeline()
.AddFileSystemOperationDocumentStorage("./PersistedQueries")
.AddMutationConventions(applyToAllMutations: true)
.AddInMemorySubscriptions()
.AddAdSpotTypes()
Expand All @@ -59,34 +82,10 @@
)
)
.AddSorting()
.RegisterDbContext<AdSpotDbContext>()
.RegisterService<IConfiguration>()
.RegisterService<IHttpContextAccessor>()
.RegisterService<InstagramService>()
.RegisterService<ConnectionRepository>()
.RegisterService<ListingRepository>()
.RegisterService<ListingTypeRepository>()
.RegisterService<OrderRepository>(ServiceKind.Resolver)
.RegisterService<PlatformRepository>()
.RegisterService<UserRepository>()
.RegisterService<FlairRepository>()
.SetPagingOptions(new HotChocolate.Types.Pagination.PagingOptions { IncludeTotalCount = true });

builder
.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
.ModifyPagingOptions(opt =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
ValidateLifetime = !isDevelopment,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(keyManager.RsaKey),
ClockSkew = TimeSpan.Zero,
};
opt.IncludeTotalCount = true;
});
builder.Services.AddAuthorization();

builder.Services.AddCors(options =>
{
Expand Down
9 changes: 3 additions & 6 deletions AdSpot.Api/Queries/UserQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,13 @@ public IQueryable<User> GetUserById(int userId, UserRepository repo)

[UseFirstOrDefault]
[UseProjection]
public async Task<IQueryable<User>?> WhoAmI(UserRepository repo, IHttpContextAccessor httpContextAccessor)
public IQueryable<User>? WhoAmI(UserRepository repo, ClaimsPrincipal claimsPrincipal)
{
var token = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
if (token is null)
var success = int.TryParse(claimsPrincipal.FindFirstValue(JwtRegisteredClaimNames.Sub), out var userId);
if (!success)
{
return null;
}
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
var userId = int.Parse(jwt.Subject);
return repo.GetUserById(userId);
}
}
4 changes: 4 additions & 0 deletions AdSpot.Models/AdSpot.Models.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="14.1.0" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions AdSpot.Models/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using HotChocolate.Authorization;
2 changes: 2 additions & 0 deletions AdSpot.Models/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ public class User
public ICollection<Connection> Connections { get; set; }
public ICollection<Listing> Listings { get; set; }
public ICollection<Order> Orders { get; set; }

[Authorize(Policy = "self")]
public ICollection<Flair> Flairs { get; set; }
}
28 changes: 10 additions & 18 deletions AdSpot.Test/TestServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,10 @@ static TestServices()
.AddProjections()
.AddFiltering()
.AddSorting()
.RegisterDbContext<AdSpotDbContext>()
.RegisterService<IConfiguration>()
.RegisterService<IHttpContextAccessor>()
.RegisterService<InstagramService>()
.RegisterService<ConnectionRepository>()
.RegisterService<ListingRepository>()
.RegisterService<ListingTypeRepository>()
.RegisterService<OrderRepository>(ServiceKind.Resolver)
.RegisterService<PlatformRepository>()
.RegisterService<UserRepository>()
.RegisterService<FlairRepository>()
.SetPagingOptions(new HotChocolate.Types.Pagination.PagingOptions { IncludeTotalCount = true, })
.ModifyPagingOptions(opt =>
{
opt.IncludeTotalCount = true;
})
.ModifyRequestOptions(options =>
{
options.IncludeExceptionDetails = true;
Expand Down Expand Up @@ -88,17 +80,17 @@ private static ClaimsPrincipal CreateClaimsPrincipal()
}

public static async Task<IExecutionResult> ExecuteRequestAsync(
Action<IQueryRequestBuilder> configureRequest,
Action<OperationRequestBuilder> configureRequest,
CancellationToken cancellationToken = default
)
{
var scope = ServiceProvider.CreateAsyncScope();

var requestBuilder = new QueryRequestBuilder();
var requestBuilder = new OperationRequestBuilder();
requestBuilder.SetServices(scope.ServiceProvider);
configureRequest(requestBuilder);
requestBuilder.AddGlobalState(nameof(ClaimsPrincipal), CreateClaimsPrincipal());
var request = requestBuilder.Create();
var request = requestBuilder.Build();

var context = ServiceProvider.GetRequiredService<AdSpotDbContext>();
context.Database.EnsureDeleted();
Expand All @@ -112,17 +104,17 @@ public static async Task<IExecutionResult> ExecuteRequestAsync(

public static async Task<IExecutionResult> ExecuteRequestAsync(
Action<AsyncServiceScope> arrangeData,
Action<IQueryRequestBuilder> configureRequest,
Action<OperationRequestBuilder> configureRequest,
CancellationToken cancellationToken = default
)
{
var scope = ServiceProvider.CreateAsyncScope();

var requestBuilder = new QueryRequestBuilder();
var requestBuilder = new OperationRequestBuilder();
requestBuilder.SetServices(scope.ServiceProvider);
configureRequest(requestBuilder);
requestBuilder.AddGlobalState(nameof(ClaimsPrincipal), CreateClaimsPrincipal());
var request = requestBuilder.Create();
var request = requestBuilder.Build();

var context = ServiceProvider.GetRequiredService<AdSpotDbContext>();
context.Database.EnsureDeleted();
Expand Down
Loading