Refactored endpoints to be more functional and more expressive
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Application.Services.Grants;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||
|
||||
@@ -13,19 +15,22 @@ public class CreateAwardEndpoint : IEndpoint
|
||||
{
|
||||
builder.MapPost("api/grants/awards", async (CreateAwardRequest request, HttpContext context,
|
||||
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||
{
|
||||
var result = await service.CreateAwardAsync(request, context.GetUserId(), cancellationToken);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok()
|
||||
: Results.BadRequest(result.Errors.ToProblems());
|
||||
})
|
||||
.WithDescription("Register an approved funding award (M01)")
|
||||
.WithName(typeof(CreateAwardEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
await service.CreateAwardAsync(request, context.GetUserId(), cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true } => Results.Ok(),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(),
|
||||
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
|
||||
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Register an approved funding award (M01)")
|
||||
.WithName(typeof(CreateAwardEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<Problems>(StatusCodes.Status400BadRequest)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Application.Services.Grants;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||
|
||||
@@ -13,18 +15,22 @@ public class CreateBudgetEndpoint : IEndpoint
|
||||
{
|
||||
builder.MapPost("api/grants/budgets", async (CreateBudgetRequest request, HttpContext context,
|
||||
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||
{
|
||||
var result = await service.CreateBudgetAsync(request, context.GetUserId(), cancellationToken);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok()
|
||||
: Results.BadRequest(result.Errors.ToProblems());
|
||||
})
|
||||
await service.CreateBudgetAsync(request, context.GetUserId(), cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true } => Results.Ok(),
|
||||
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(),
|
||||
var r when r.HasError<CreateError>() => Results.BadRequest(),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Create a new budget line (M05)")
|
||||
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.Produces<Problems>(StatusCodes.Status400BadRequest)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Application.Services.Grants;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||
|
||||
@@ -13,19 +15,20 @@ public class CreateContractEndpoint : IEndpoint
|
||||
{
|
||||
builder.MapPost("api/grants/contracts", async (CreateContractRequest request, HttpContext context,
|
||||
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||
{
|
||||
var result = await service.CreateContractAsync(request, context.GetUserId(), cancellationToken);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok()
|
||||
: Results.BadRequest(result.Errors.ToProblems());
|
||||
})
|
||||
.WithDescription("Create a contract record from an approved award (M02)")
|
||||
.WithName(typeof(CreateContractEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
await service.CreateContractAsync(request, context.GetUserId(), cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true } => Results.Ok(),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(),
|
||||
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Create a contract record from an approved award (M02)")
|
||||
.WithName(typeof(CreateContractEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces<Problems>(StatusCodes.Status400BadRequest)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Application.Services.Grants;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||
|
||||
@@ -13,19 +15,19 @@ public class ExecuteContractEndpoint : IEndpoint
|
||||
{
|
||||
builder.MapPost("api/grants/contracts/execute", async (ExecuteContractRequest request, HttpContext context,
|
||||
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||
{
|
||||
var result = await service.ExecuteContractAsync(request, context.GetUserId(), cancellationToken);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok()
|
||||
: Results.BadRequest(result.Errors.ToProblems());
|
||||
})
|
||||
.WithDescription("Execute the contract and lock the contractual version")
|
||||
.WithName(typeof(ExecuteContractEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
await service.ExecuteContractAsync(request, context.GetUserId(), cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true } => Results.Ok(),
|
||||
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Execute the contract and lock the contractual version")
|
||||
.WithName(typeof(ExecuteContractEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces<Problems>(StatusCodes.Status400BadRequest)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-10
@@ -1,32 +1,36 @@
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Application.Services.Grants;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Models;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||
|
||||
[ApiVersionTarget(1)]
|
||||
public class GetAwardByIdEndpoint : IEndpoint
|
||||
public class GetAwardEndpoint : IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder builder)
|
||||
{
|
||||
builder.MapGet("api/grants/awards/{awardId}", async (string awardId, GrantsService service,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
{
|
||||
var result = await service.GetAwardByIdAsync(awardId, cancellationToken);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok(result.Value)
|
||||
: Results.NotFound();
|
||||
})
|
||||
await service.GetAwardAsync(awardId, cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var award } => Results.Ok(award),
|
||||
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get award by ID")
|
||||
.WithName(typeof(GetAwardByIdEndpoint).ToEndpointName())
|
||||
.WithName(typeof(GetAwardEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Award>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Models;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||
|
||||
@@ -15,18 +16,16 @@ public class GetAwardsEndpoint : IEndpoint
|
||||
{
|
||||
builder.MapGet("api/grants/awards", async (GrantsService service,
|
||||
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||
{
|
||||
var result = await service.GetAwardsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok(result.Value)
|
||||
: Results.BadRequest(result.Errors.ToProblems());
|
||||
})
|
||||
await service.GetAwardsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var awards } => Results.Ok(awards),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of awards")
|
||||
.WithName(typeof(GetAwardsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Award[]>(StatusCodes.Status200OK)
|
||||
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
}
|
||||
|
||||
+13
-9
@@ -1,32 +1,36 @@
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Application.Services.Grants;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Models;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||
|
||||
[ApiVersionTarget(1)]
|
||||
public class GetBudgetByIdEndpoint : IEndpoint
|
||||
public class GetBudgetEndpoint : IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder builder)
|
||||
{
|
||||
builder.MapGet("api/grants/budgets/{budgetId}", async (string budgetId, GrantsService service,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
{
|
||||
var result = await service.GetBudgetByIdAsync(budgetId, cancellationToken);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok(result.Value)
|
||||
: Results.NotFound();
|
||||
})
|
||||
await service.GetBudgetAsync(budgetId, cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var budget } => Results.Ok(budget),
|
||||
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get budget by ID")
|
||||
.WithName(typeof(GetBudgetByIdEndpoint).ToEndpointName())
|
||||
.WithName(typeof(GetBudgetEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Budget>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Models;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||
|
||||
@@ -15,18 +16,16 @@ public class GetBudgetsEndpoint : IEndpoint
|
||||
{
|
||||
builder.MapGet("api/grants/budgets", async (GrantsService service,
|
||||
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||
{
|
||||
var result = await service.GetBudgetsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok(result.Value)
|
||||
: Results.BadRequest(result.Errors.ToProblems());
|
||||
})
|
||||
await service.GetBudgetsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var budgets } => Results.Ok(budgets),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get all budgets")
|
||||
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Budget[]>(StatusCodes.Status200OK)
|
||||
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
}
|
||||
|
||||
+13
-9
@@ -1,32 +1,36 @@
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Application.Services.Grants;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Models;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||
|
||||
[ApiVersionTarget(1)]
|
||||
public class GetContractByIdEndpoint : IEndpoint
|
||||
public class GetContractEndpoint : IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder builder)
|
||||
{
|
||||
builder.MapGet("api/grants/contracts/{contractId}", async (string contractId, GrantsService service,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
{
|
||||
var result = await service.GetContractByIdAsync(contractId, cancellationToken);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok(result.Value)
|
||||
: Results.NotFound();
|
||||
})
|
||||
await service.GetContractAsync(contractId, cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var contract } => Results.Ok(contract),
|
||||
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get contract by ID")
|
||||
.WithName(typeof(GetContractByIdEndpoint).ToEndpointName())
|
||||
.WithName(typeof(GetContractEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Contract>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Models;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||
|
||||
@@ -15,19 +16,17 @@ public class GetContractsEndpoint : IEndpoint
|
||||
{
|
||||
builder.MapGet("api/grants/contracts", async (GrantsService service,
|
||||
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||
{
|
||||
var result = await service.GetContractsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok(result.Value)
|
||||
: Results.BadRequest(result.Errors.ToProblems());
|
||||
})
|
||||
.WithDescription("Get all contracts")
|
||||
.WithName(typeof(GetContractsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Contract[]>(StatusCodes.Status200OK)
|
||||
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
await service.GetContractsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var contracts } => Results.Ok(contracts),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get all contracts")
|
||||
.WithName(typeof(GetContractsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Contract[]>(StatusCodes.Status200OK)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Grants);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user