Insert admin user on first boot - c#

I am creating a Blazor app with basic authentication.
The problem is the following: When I launch the application, I cannot access anything, I am not connected (registration is only done via a user who has the right). So I want to create an admin: admin account when the database is initialized.
Here is what I did:
Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, UserManager<T_Utilisateur> _userManager){
...
app.InitBaseDeDonnes(Configuration["DefaultApplicativePassword"], _userManager);
...
}
InitData.cs
public async static void InitBaseDeDonnes(this IApplicationBuilder _app, string _defaultPassword, UserManager<T_Utilisateur> _userManager){
...
await _userManager.CreateAsync(user, "admin");
...
}
But here is the error that returned to me (about the "_userManager" object) :
Cannot access a disposed object.
Do you know which method should I use to create this admin user?

Extracting the manager from the services is a possibility to access the class allowing this to manage the users. Pass this class from Startup.cs not working.
public async static void InitBaseDeDonnes(this IApplicationBuilder _app, string _defaultPassword) {
...
int nbUser = await UtilisateurDal.CountAsync();
if (nbUser < 1) {
using (IServiceScope scope = _app.ApplicationServices.CreateScope()) {
UserManager<T_Utilisateur> userManager = scope.ServiceProvider.GetRequiredService<UserManager<T_Utilisateur>>();
Task.Run(() => InitAdminUser(userManager, "admin#domain.com")).Wait();
}
}
...
}
private static async Task InitAdminUser(UserManager<T_Utilisateur> _userManager, string _email) {
T_Utilisateur user = new T_Utilisateur {
Id = Guid.NewGuid(),
UserName = _email,
Email = _email,
EmailConfirmed = true,
DateDeCreation = DateTime.Now,
SecurityStamp = Guid.NewGuid().ToString()
};
await _userManager.CreateAsync(user, Constants.MotDePasseDefaut);
}

Related

Custom log for each user with Serilog

I am trying to create a custom logger based on serilog. The application i am building is based on net6 blazor-server-side.
The goal is that every time a user logs into the application, I create a specific log file for him.
First I create a dependency injection in the program.cs
file Program.cs
builder.Services.AddScoped<ICustomLogger>( s => new CustomLogger());
In the Customlogger class, I initialize the loggerconfiguration in the constructor
file CustomLogger.cs
private ILogger<CustomLogger> _logger;
protected readonly LoggerConfiguration _loggerConfig;
public CustomLogger()
{
_loggerConfig = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Debug();
}
In the Login.razor , once the login is successful, I call the CreateLogger method, passing the username as a parameter (this is to create a specific folder)
file CustomLogger.cs
public void CreateLogger(string username)
{
var l = _loggerConfig.WriteTo.File($"./Logs/{username}/log_.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 30).CreateLogger();
_logger = new SerilogLoggerFactory(l).CreateLogger<CustomLogger>(); // creates an instance of ILogger<CustomLogger>
}
Beyond that, I've created methods to write the various log levels
file CustomLogger.cs
public void LogInformation(string m)
{
_logger.LogInformation(m);
}
public void LogError(string m)
{
_logger.LogError(m);
}
public void LogWarning(string m)
{
_logger.LogWarning(m);
}
The Customlogger class is bound to the ICustomLogger interface
file ICustomLogger.cs
public interface ICustomLogger
{
void LogInformation(string m);
void LogError(string m);
void LogWarning(string m);
void CreateLogger(string username);
ILogger<CustomLogger> GetLogger();
}
For the moment I see that the system works, if I connect with a user, his folder and the file are created, and so on for each user.
My question is :
Could this approach cause problems?
Is it already possible to do this via Serilog?
Thanks for your time
N.
UPDATE
the system works well in the login page, for each user to create his own logger, but as soon as I move to the index page, the constructor of the CustomLogger class is called and the ILogger is null.
I thought AddScoped was only called once for "session"
Is it possible to call AddSingleton every time the user logs in, that way the specific dependency remains for as long as needed?
UPDATE 2
I changed the injection, now I use AddSingleton
Program.cs
builder.Services.AddSingleton<IOramsLoggerService>(s => new OramsLoggerService());
Inside the OramsLoggerService class, I created a list of loggers, which is filled at each login
OramsLoggerSerivce.cs
public class OramsLoggerService : IOramsLoggerService
{
private List<OramsLogger> loggers;
public OramsLoggerService()
{
loggers = new List<OramsLogger>();
}
public void CreateLogger(string? username)
{
if (string.IsNullOrEmpty(username))
{
throw new ArgumentNullException("username");
}
if (loggers.Where(x => x.Username == username).Count() > 0)
{
return;
}
// characters not allowed in the folder name
string originaUsername = username;
username = username.Replace("<", "-");
username = username.Replace(">", "-");
username = username.Replace(":", "-");
username = username.Replace("/", "-");
username = username.Replace("\\", "-");
username = username.Replace("|", "-");
username = username.Replace("?", "-");
username = username.Replace("*", "-");
username = username.Replace("\"", "-");
var loggerConfig = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Debug();
var l = loggerConfig.WriteTo.File($"./Logs/{username}/log_.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 100).CreateLogger();
var logger = new SerilogLoggerFactory(l).CreateLogger<OramsLogger>(); // creates an instance of ILogger<OramsLogger>
loggers.Add(new OramsLogger(logger, originaUsername));
}
}
by doing so, I have all the loggers created in the other pages available.
The OramsLogger class contains the logger and the username.
I use the username to search the list, I think I'll change it to the id in the future.
OramsLogger.cs
public class OramsLogger
{
public ILogger<OramsLogger> logger;
public string Username { get; set; }
public OramsLogger(ILogger<OramsLogger> l, string username)
{
logger = l;
Username = username;
}
}
For the moment I have created 1000 dummy users when I login with my user and it seems to work.
Could this be a good approach?
For user-level custom logs, this method is appropriate.
There are no particular concerns.

WebApplicationFactory is running the Api with the old IConnectionMultiplexer which fails to connect instead of first overriding configuration

I'm working on an integration test for a Web API which communicates through Redis, so I tried to replace the Redis Server with a containerized one and run some tests.
The issue is that it is first running the Api with project's appsettings.Development.json configuration and the old IConnectionMultiplexer instance which obviously won't connect because the hostname is offline. The question is how do I make it run the project with the new IConnectionMultiplexer that uses the containerized Redis Server? Basically the sequence is wrong there. What I did is more like run the old IConnectionMultiplexer and replace it with the new one but it wouldn't connect to the old one, so that exception prevents me from continuing. I commented the line of code where it throws the exception but as I said it's obvious because it's first running the Api with the old configuration instead of first overriding the configuration and then running the Api.
I could have done something like the following but I'm DI'ing other services based on configuration as well, meaning I must override the configuration first and then run the actual API code.
try
{
var redis = ConnectionMultiplexer.Connect(redisConfig.Host);
serviceCollection.AddSingleton<IConnectionMultiplexer>(redis);
}
catch
{
// We discard that service if it's unable to connect
}
Api
public static class RedisConnectionConfiguration
{
public static void AddRedisConnection(this IServiceCollection serviceCollection, IConfiguration config)
{
var redisConfig = config.GetSection("Redis").Get<RedisConfiguration>();
serviceCollection.AddHostedService<RedisSubscription>();
serviceCollection.AddSingleton(redisConfig);
var redis = ConnectionMultiplexer.Connect(redisConfig.Host); // This fails because it didn't override Redis:Host
serviceCollection.AddSingleton<IConnectionMultiplexer>(redis);
}
}
Integration tests
public class OrderManagerApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
private const string Password = "Test1234!";
private readonly TestcontainersContainer _redisContainer;
private readonly int _externalPort = Random.Shared.Next(10_000, 60_000);
public OrderManagerApiFactory()
{
_redisContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("redis:alpine")
.WithEnvironment("REDIS_PASSWORD", Password)
.WithPortBinding(_externalPort, 6379)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
.Build();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development");
builder.ConfigureLogging(logging =>
{
logging.ClearProviders();
});
builder.ConfigureAppConfiguration(config =>
{
config.AddInMemoryCollection(new Dictionary<string, string>
{
{ "Redis:Host", $"localhost:{_externalPort},password={Password},allowAdmin=true" },
{ "Redis:Channels:Main", "main:new:order" },
});
});
builder.ConfigureTestServices(services =>
{
services.RemoveAll(typeof(IConnectionMultiplexer));
services.AddSingleton<IConnectionMultiplexer>(_ =>
ConnectionMultiplexer.Connect($"localhost:{_externalPort},password={Password},allowAdmin=true"));
});
}
public async Task InitializeAsync()
{
await _redisContainer.StartAsync();
}
public new async Task DisposeAsync()
{
await _redisContainer.DisposeAsync();
}
}
public class OrderManagerTests : IClassFixture<OrderManagerApiFactory>, IAsyncLifetime
{
private readonly OrderManagerApiFactory _apiFactory;
public OrderManagerTests(OrderManagerApiFactory apiFactory)
{
_apiFactory = apiFactory;
}
[Fact]
public async Task Test()
{
// Arrange
var configuration = _apiFactory.Services.GetRequiredService<IConfiguration>();
var redis = _apiFactory.Services.GetRequiredService<IConnectionMultiplexer>();
var channel = configuration.GetValue<string>("Redis:Channels:Main");
// Act
await redis.GetSubscriber().PublishAsync(channel, "ping");
// Assert
}
public Task InitializeAsync()
{
return Task.CompletedTask;
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}
}
Problem solved.
If you override WebApplicationFactory<T>.CreateHost() and call IHostBuilder.ConfigureHostConfiguration() before calling base.CreateHost() the configuration you add will be visible between WebApplication.CreateBuilder() and builder.Build().
The following two links might help someone:
https://github.com/dotnet/aspnetcore/issues/37680
https://github.com/dotnet/aspnetcore/issues/9275
public sealed class OrderManagerApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
private const string Password = "Test1234!";
private const int ExternalPort = 7777; // Random.Shared.Next(10_000, 60_000);
private readonly TestcontainersContainer _redisContainer;
public OrderManagerApiFactory()
{
_redisContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("redis:alpine")
.WithEnvironment("REDIS_PASSWORD", Password)
.WithPortBinding(ExternalPort, 6379)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
.Build();
}
public async Task InitializeAsync()
{
await _redisContainer.StartAsync();
}
public new async Task DisposeAsync()
{
await _redisContainer.DisposeAsync();
}
protected override IHost CreateHost(IHostBuilder builder)
{
builder.ConfigureHostConfiguration(config =>
config.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Redis:Host", $"localhost:{ExternalPort},password={Password},allowAdmin=true"),
new KeyValuePair<string, string>("Redis:Channels:Main", "main:new:order")
}));
return base.CreateHost(builder);
}
}

create an API with .NET Minimal APIs that require session api key

This video is really nice and shows how to create Minimal APIs using .net 6:
https://www.youtube.com/watch?v=eRJFNGIsJEo
It is amazing how it uses dependency injection to get mostly everything that you need inside your endpoints. For example if I need the value of a custom header I would have this:
app.MapGet("/get-custom-header", ([FromHeader(Name = "User-Agent")] string data) =>
{
return $"User again is: {data}";
});
I can have another endpoint where I have access to the entire httpContext like this:
app.MapGet("/foo", (Microsoft.AspNetCore.Http.HttpContext c) =>
{
var path = c.Request.Path;
return path;
});
I can even register my own classes with this code: builder.Services.AddTransient<TheClassIWantToRegister>()
If I register my custom classes I will be able to create an instance of that class every time I need it on and endpoint (app.MapGet("...)
Anyways back to the question. When a user logs in I send him this:
{
"ApiKey": "1234",
"ExpirationDate": blabla bla
.....
}
The user must send the 1234 token to use the API. How can I avoid repeating my code like this:
app.MapGet("/getCustomers", ([FromHeader(Name = "API-KEY")] string apiToken) =>
{
// validate apiToken agains DB
if(validationPasses)
return Database.Customers.ToList();
else
// return unauthorized
});
I have tried creating a custom class RequiresApiTokenKey and registering that class as builder.Services.AddTransient<RequiresApiTokenKey>() so that my API knows how to create an instance of that class when needed but how can I access the current http context inside that class for example? How can I avoid having to repeat having to check if the header API-KEY header is valid in every method that requires it?
Gave this a test based on my comments.
This would call the method Invoke in the middleware on each request and you can do checks here.
Probably a better way would be to use the AuthenticationHandler. using this would mean you can attribute individual endpoints to have the API key check done instead of all incoming requests
But, I thought this was still useful, middleware can be used for anything you'd like to perform on every request
Using Middleware
Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
//our custom middleware extension to call UseMiddleware
app.UseAPIKeyCheckMiddleware();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapGet("/", () => "Hello World!");
app.Run();
APIKeyCheckMiddleware.cs
using Microsoft.Extensions.Primitives;
internal class APIKeyCheckMiddleware
{
private readonly RequestDelegate _next;
public APIKeyCheckMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
//we could inject here our database context to do checks against the db
if (httpContext.Request.Headers.TryGetValue("API-KEY", out StringValues value))
{
//do the checks on key
var apikey = value;
}
else
{
//return 403
httpContext.Response.StatusCode = 403;
}
await _next(httpContext);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class APIKeyCheckMiddlewareExtensions
{
public static IApplicationBuilder UseAPIKeyCheckMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<APIKeyCheckMiddleware>();
}
}
I used SmithMart's answer but had to change things in the Invoke method and used DI in the constructor.
Here's my version:
internal class ApiKeyCheckMiddleware
{
public static string ApiKeyHeaderName = "X-ApiKey";
private readonly RequestDelegate _next;
private readonly ILogger<ApiKeyCheckMiddleware> _logger;
private readonly IApiKeyService _apiKeyService;
public ApiKeyCheckMiddleware(RequestDelegate next, ILogger<ApiKeyCheckMiddleware> logger, IApiKeyService apiKeyService)
{
_next = next;
_logger = logger;
_apiKeyService = apiKeyService;
}
public async Task InvokeAsync(HttpContext httpContext)
{
var request = httpContext.Request;
var hasApiKeyHeader = request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyValue);
if (hasApiKeyHeader)
{
_logger.LogDebug("Found the header {ApiKeyHeader}. Starting API Key validation", ApiKeyHeaderName);
if (apiKeyValue.Count != 0 && !string.IsNullOrWhiteSpace(apiKeyValue))
{
if (Guid.TryParse(apiKeyValue, out Guid apiKey))
{
var allowed = await _apiKeyService.Validate(apiKey);
if (allowed)
{
_logger.LogDebug("Client successfully logged in with key {ApiKey}", apiKeyValue);
var apiKeyClaim = new Claim("ApiKey", apiKeyValue);
var allowedSiteIdsClaim = new Claim("SiteIds", string.Join(",", allowedSiteIds));
var principal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> { apiKeyClaim, allowedSiteIdsClaim }, "ApiKey"));
httpContext.User = principal;
await _next(httpContext);
return;
}
}
_logger.LogWarning("Client with ApiKey {ApiKey} is not authorized", apiKeyValue);
}
else
{
_logger.LogWarning("{HeaderName} header found, but api key was null or empty", ApiKeyHeaderName);
}
}
else
{
_logger.LogWarning("No ApiKey header found.");
}
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
}

In a Blazor Server app, is it possible to inject a GraphServiceClient into a scoped service?

I've been experimenting with the new Blazor features and I'm attempting to pull user data from our Azure AD into a test app. These are the relevant snippets:
My Service
public class UserService
{
GraphServiceClient _graphClient { get; set; }
protected User _user = null;
public UserService(GraphServiceClient graphClient)
{
_graphClient = graphClient;
}
public string GetUserName()
{
return User()?.DisplayName ?? "";
}
Startup
public void ConfigureServices(IServiceCollection services)
{
var initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
// Add sign-in with Microsoft
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
// Add the possibility of acquiring a token to call a protected web API
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
// Enables controllers and pages to get GraphServiceClient by dependency injection
// And use an in memory token cache
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
services.AddRazorPages()
.AddMicrosoftIdentityUI();
services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
services.AddSingleton<WeatherForecastService>();
services.AddScoped<UserService>();
The GraphServiceClient does get initialized in my .cs script but I get the error message:
Error : No account or login hint was passed to the AcquireTokenSilent call
Its not a problem (I think) with any azure configuration as everything works fine if I use the Microsoft sample and make a ComponentBase.
public class UserProfileBase : ComponentBase
{
[Inject]
GraphServiceClient GraphClient { get; set; }
protected User _user = new User();
protected override async Task OnInitializedAsync()
{
await GetUserProfile();
}
/// <summary>
/// Retrieves user information from Microsoft Graph /me endpoint.
/// </summary>
/// <returns></returns>
private async Task GetUserProfile()
{
try
{
var request = GraphClient.Me.Request();
_user = await request.GetAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
My current thought is that the Authorize tag that the profile component uses (and thus the ComponentBase?) is doing something behind the scenes with the access token even though I am already authenticated?
#page "/profile"
#using Microsoft.AspNetCore.Authorization
#attribute [Authorize]
#inherits UserProfileBase
public class UserAccount
{
public int Id {get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
public string Email {get; set;}
}
public class UserService
{
GraphServiceClient _graphClient;
protected UserAccount _user {get; set;}
public UserService(GraphServiceClient graphClient)
{
_graphClient = graphClient;
}
public async Task<string> GetUserName()
{
UserAccount = await GetUserAsync();
return $"{UserAccount.FirstName} {UserAccount.LastName}";
}
public async Task<UserAccount> GetUserAsync()
{
var user = awiat __graphClient.Me.Request.Select( e => new
{
e.Id,
e.GivenName,
e.Surname,
e.Identities,
}).GetAsync();
if(user != null)
{
var email = user.Identities.ToList().FirstOrDefault(x => x.SignInType == "emailAddress")?.IssuerAssignedId;
return new UserAccount
{
Id= user.Id,
FirstName= user.GivenName,
LastName= user.Surname,
Email= email
};
}
else {return null;}
}
}
Sorry this answer is over a year late - hopefully this will help someone else in the future. I was trying to solve this exact issue today too, and I got the missing pieces of this puzzle from this demo project.
In addition to your ConfigureServices method, you need to make sure that you have controllers mapped in your endpoints so that the Identity UI can map responses.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers(); // Important for Microsoft Identity UI
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
And then you need to have some exception handling on your scoped service. I'm pretty sure this is because of Blazor's pre-rendering feature not initially authenticating the user in the first render. But don't quote me on that 😅
I can't see enough of the OP's service, so here's mine:
using Microsoft.Graph;
using Microsoft.Identity.Web;
namespace MyProject.Services
{
public class UserService
{
private readonly GraphServiceClient _graphServiceClient;
private readonly MicrosoftIdentityConsentAndConditionalAccessHandler _consentHandler;
public UserService(
GraphServiceClient graphServiceClient,
MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler)
{
_graphServiceClient = graphServiceClient;
_consentHandler = consentHandler;
}
public async Task<User?> GetUserAsync()
{
try
{
return await _graphServiceClient.Me.Request().GetAsync();
}
catch (Exception ex)
{
_consentHandler.HandleException(ex);
return null;
}
}
}
}

Validating user in Azure Mobile App Against Exiting User Database

I am writing my first Azure Mobile App and I want to implement "Custom Authentication" against an existing websites user database.
In the existing ASP.Net website I have the usual dbo.AspNetUsers tables etc.
I cannot work out how to call this existing website to authenticate a user.
I have the following code but I am lost how to get the isValidAssertion function to talk to my existing user database from within the Axure Mobile App.
It would be the equivalent of this line found in the website..
ApplicationSignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
So, I have the following code:
private static bool isValidAssertion(JObject assertion)
{
// this is where I want to call the existing user database
// this is how it's done in the MVC website
//ApplicationSignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
return true;
}
public IHttpActionResult Post([FromBody] JObject assertion)
{
if (isValidAssertion(assertion)) // user-defined function, checks against a database
{
JwtSecurityToken token = AppServiceLoginHandler.CreateToken(new Claim[] { new Claim(JwtRegisteredClaimNames.Sub, (string)assertion["username"]) },
mySigningKey,
myAppURL,
myAppURL,
TimeSpan.FromHours(24));
return Ok(new LoginResult()
{
AuthenticationToken = token.RawData,
User = new LoginResultUser() { UserId = (string)assertion["username"] }
});
}
else // user assertion was not valid
{
return ResponseMessage(Request.CreateUnauthorizedResponse());
}
}
Can anyone point me in the right direction please?
You would need to use Asp.Net Identity for this.
Basically you need to create BaseApiController to get OWIN Context first:
public class BaseApiController : ApiController
{
private ApplicationUserManager _appUserManager = null;
protected ApplicationUserManager AppUserManager
{
get
{
return _appUserManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
}
}
Then in your custom authentication controller need to inherit from BaseApiController:
[MobileAppController]
public class CustomAuthController : BaseApiController
{
private static bool isValidAssertion(JObject assertion)
{
var username = assertion["username"].Value<string>();
var password = assertion["password"].Value<string>();
//Validate user using FindAsync() method
var user = await this.AppUserManager.FindAsync(username, password);
return (user != null);
}
}
You will also need to init OWIN context in Startup class:
[assembly: OwinStartup(typeof(UPARMobileApp.Startup))]
namespace UPARMobileApp
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOWinContext(app);
ConfigureMobileApp(app);
}
}
}
public static void ConfigureOWinContext(IAppBuilder app)
{
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
}
More information about how to setup ApplicationDbContext, ApplicationUserManager and OWIN configuration can be read from here:
http://bitoftech.net/2015/01/21/asp-net-identity-2-with-asp-net-web-api-2-accounts-management/

Categories