Add Admin roles
This commit is contained in:
80
src/Managing.Api/ADMIN_FEATURE.md
Normal file
80
src/Managing.Api/ADMIN_FEATURE.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Admin Feature Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The admin feature allows specific users to manage all bots in the system, regardless of ownership. Admin users can start, stop, delete, and modify any bot without owning the associated account.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Admin privileges are granted through environment variables, making it secure and environment-specific. The system checks if a user is an admin by comparing their username against a comma-separated list of admin usernames configured in the environment.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variable
|
||||||
|
Set the `AdminUsers` environment variable with a comma-separated list of usernames:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AdminUsers=admin1,superuser,john.doe
|
||||||
|
```
|
||||||
|
|
||||||
|
### CapRover Configuration
|
||||||
|
In your CapRover dashboard:
|
||||||
|
1. Go to your app's settings
|
||||||
|
2. Navigate to "Environment Variables"
|
||||||
|
3. Add a new environment variable:
|
||||||
|
- Key: `AdminUsers`
|
||||||
|
- Value: `admin1,superuser,john.doe` (replace with actual admin usernames)
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
For local development, you can set this in your `appsettings.Development.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"AdminUsers": "admin1,superuser,john.doe"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Admin Capabilities
|
||||||
|
|
||||||
|
Admin users can perform all bot operations without ownership restrictions:
|
||||||
|
|
||||||
|
- **Start/Save Bot**: Create and start bots for any account
|
||||||
|
- **Stop Bot**: Stop any running bot
|
||||||
|
- **Delete Bot**: Delete any bot
|
||||||
|
- **Restart Bot**: Restart any bot
|
||||||
|
- **Open/Close Positions**: Manually open or close positions for any bot
|
||||||
|
- **Update Configuration**: Modify any bot's configuration
|
||||||
|
- **View Bot Configuration**: Access any bot's configuration details
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
1. **Environment-Based**: Admin users are configured via environment variables, not through the API
|
||||||
|
2. **No Privilege Escalation**: Regular users cannot grant themselves admin access
|
||||||
|
3. **Audit Logging**: All admin actions are logged with the admin user's context
|
||||||
|
4. **Case-Insensitive**: Username matching is case-insensitive for convenience
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
The admin feature is implemented using:
|
||||||
|
- `IAdminConfigurationService`: Checks if a user is an admin
|
||||||
|
- Updated `UserOwnsBotAccount` method: Returns true for admin users
|
||||||
|
- Dependency injection: Service is registered as a singleton
|
||||||
|
- Configuration reading: Reads from `AdminUsers` environment variable
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
1. **Set Admin Users**:
|
||||||
|
```bash
|
||||||
|
AdminUsers=alice,bob,charlie
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Admin Operations**:
|
||||||
|
- Alice, Bob, or Charlie can now manage any bot in the system
|
||||||
|
- They can use all existing bot endpoints without ownership restrictions
|
||||||
|
- All operations are logged with their username for audit purposes
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Admin not working**: Check if the username exactly matches the configuration (case-insensitive)
|
||||||
|
- **No admins configured**: Check the `AdminUsers` environment variable is set
|
||||||
|
- **Multiple environments**: Each environment (dev, staging, prod) should have its own admin configuration
|
||||||
@@ -4,6 +4,7 @@ using Managing.Application.Abstractions;
|
|||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.Hubs;
|
using Managing.Application.Hubs;
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
|
using Managing.Application.Shared;
|
||||||
using Managing.Common;
|
using Managing.Common;
|
||||||
using Managing.Core;
|
using Managing.Core;
|
||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
@@ -40,6 +41,7 @@ public class BotController : BaseController
|
|||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly IMoneyManagementService _moneyManagementService;
|
private readonly IMoneyManagementService _moneyManagementService;
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly IAdminConfigurationService _adminService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BotController"/> class.
|
/// Initializes a new instance of the <see cref="BotController"/> class.
|
||||||
@@ -56,7 +58,7 @@ public class BotController : BaseController
|
|||||||
public BotController(ILogger<BotController> logger, IMediator mediator, IHubContext<BotHub> hubContext,
|
public BotController(ILogger<BotController> logger, IMediator mediator, IHubContext<BotHub> hubContext,
|
||||||
IBacktester backtester, IBotService botService, IUserService userService,
|
IBacktester backtester, IBotService botService, IUserService userService,
|
||||||
IAccountService accountService, IMoneyManagementService moneyManagementService,
|
IAccountService accountService, IMoneyManagementService moneyManagementService,
|
||||||
IServiceScopeFactory scopeFactory) : base(userService)
|
IServiceScopeFactory scopeFactory, IAdminConfigurationService adminService) : base(userService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -66,6 +68,7 @@ public class BotController : BaseController
|
|||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_moneyManagementService = moneyManagementService;
|
_moneyManagementService = moneyManagementService;
|
||||||
_scopeFactory = scopeFactory;
|
_scopeFactory = scopeFactory;
|
||||||
|
_adminService = adminService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -73,7 +76,7 @@ public class BotController : BaseController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="identifier">The identifier of the bot to check</param>
|
/// <param name="identifier">The identifier of the bot to check</param>
|
||||||
/// <param name="accountName">Optional account name to check when creating a new bot</param>
|
/// <param name="accountName">Optional account name to check when creating a new bot</param>
|
||||||
/// <returns>True if the user owns the account, False otherwise</returns>
|
/// <returns>True if the user owns the account or is admin, False otherwise</returns>
|
||||||
private async Task<bool> UserOwnsBotAccount(Guid identifier, string accountName = null)
|
private async Task<bool> UserOwnsBotAccount(Guid identifier, string accountName = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -82,6 +85,9 @@ public class BotController : BaseController
|
|||||||
if (user == null)
|
if (user == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Admin users can access all bots
|
||||||
|
if (_adminService.IsUserAdmin(user.Name))
|
||||||
|
return true;
|
||||||
|
|
||||||
if (identifier != default)
|
if (identifier != default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Application.Abstractions;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Application.Shared;
|
||||||
using Managing.Application.Trading.Commands;
|
using Managing.Application.Trading.Commands;
|
||||||
using Managing.Domain.MoneyManagements;
|
using Managing.Domain.MoneyManagements;
|
||||||
using Managing.Domain.Trades;
|
using Managing.Domain.Trades;
|
||||||
@@ -26,6 +27,8 @@ public class TradingController : BaseController
|
|||||||
private readonly IMoneyManagementService _moneyManagementService;
|
private readonly IMoneyManagementService _moneyManagementService;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly ILogger<TradingController> _logger;
|
private readonly ILogger<TradingController> _logger;
|
||||||
|
private readonly IAdminConfigurationService _adminService;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TradingController"/> class.
|
/// Initializes a new instance of the <see cref="TradingController"/> class.
|
||||||
@@ -35,13 +38,16 @@ public class TradingController : BaseController
|
|||||||
/// <param name="closeTradeCommandHandler">Command handler for closing trades.</param>
|
/// <param name="closeTradeCommandHandler">Command handler for closing trades.</param>
|
||||||
/// <param name="tradingService">Service for trading operations.</param>
|
/// <param name="tradingService">Service for trading operations.</param>
|
||||||
/// <param name="mediator">Mediator for handling commands and requests.</param>
|
/// <param name="mediator">Mediator for handling commands and requests.</param>
|
||||||
|
/// <param name="adminService">Service for checking admin privileges.</param>
|
||||||
|
/// <param name="accountService">Service for account operations.</param>
|
||||||
public TradingController(
|
public TradingController(
|
||||||
ILogger<TradingController> logger,
|
ILogger<TradingController> logger,
|
||||||
ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler,
|
ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler,
|
||||||
ICommandHandler<ClosePositionCommand, Position> closeTradeCommandHandler,
|
ICommandHandler<ClosePositionCommand, Position> closeTradeCommandHandler,
|
||||||
ITradingService tradingService,
|
ITradingService tradingService,
|
||||||
IMediator mediator, IMoneyManagementService moneyManagementService,
|
IMediator mediator, IMoneyManagementService moneyManagementService,
|
||||||
IUserService userService) : base(userService)
|
IUserService userService, IAdminConfigurationService adminService,
|
||||||
|
IAccountService accountService) : base(userService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_openTradeCommandHandler = openTradeCommandHandler;
|
_openTradeCommandHandler = openTradeCommandHandler;
|
||||||
@@ -49,6 +55,8 @@ public class TradingController : BaseController
|
|||||||
_tradingService = tradingService;
|
_tradingService = tradingService;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_moneyManagementService = moneyManagementService;
|
_moneyManagementService = moneyManagementService;
|
||||||
|
_adminService = adminService;
|
||||||
|
_accountService = accountService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -149,6 +157,7 @@ public class TradingController : BaseController
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a Privy wallet address for the user.
|
/// Initializes a Privy wallet address for the user.
|
||||||
|
/// Only admins can initialize any address, regular users can only initialize their own addresses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="publicAddress">The public address of the Privy wallet to initialize.</param>
|
/// <param name="publicAddress">The public address of the Privy wallet to initialize.</param>
|
||||||
/// <returns>The initialization response containing success status and transaction hashes.</returns>
|
/// <returns>The initialization response containing success status and transaction hashes.</returns>
|
||||||
@@ -162,6 +171,18 @@ public class TradingController : BaseController
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var user = await GetUser();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Unauthorized("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has permission to initialize this address
|
||||||
|
if (!await CanUserInitializeAddress(user.Name, publicAddress))
|
||||||
|
{
|
||||||
|
return Forbid("You don't have permission to initialize this wallet address. You can only initialize your own wallet addresses.");
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _tradingService.InitPrivyWallet(publicAddress);
|
var result = await _tradingService.InitPrivyWallet(publicAddress);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
@@ -175,4 +196,42 @@ public class TradingController : BaseController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the user can initialize the given public address.
|
||||||
|
/// Admins can initialize any address, regular users can only initialize their own addresses.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userName">The username to check</param>
|
||||||
|
/// <param name="publicAddress">The public address to initialize</param>
|
||||||
|
/// <returns>True if the user can initialize the address, false otherwise</returns>
|
||||||
|
private async Task<bool> CanUserInitializeAddress(string userName, string publicAddress)
|
||||||
|
{
|
||||||
|
// Admin users can initialize any address
|
||||||
|
if (_adminService.IsUserAdmin(userName))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Admin user {UserName} initializing address {Address}", userName, publicAddress);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Regular users can only initialize their own addresses
|
||||||
|
// Check if the address belongs to one of the user's accounts
|
||||||
|
var account = await _accountService.GetAccountByKey(publicAddress, true, false);
|
||||||
|
|
||||||
|
if (account?.User?.Name == userName)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("User {UserName} initializing their own address {Address}", userName, publicAddress);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning("User {UserName} attempted to initialize address {Address} that doesn't belong to them", userName, publicAddress);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Unable to verify ownership of address {Address} for user {UserName}", publicAddress, userName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
125
src/Managing.Api/TRADING_CONTROLLER_SECURITY.md
Normal file
125
src/Managing.Api/TRADING_CONTROLLER_SECURITY.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# TradingController Security Enhancement
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `InitPrivyWallet` endpoint in `TradingController` has been enhanced with admin role security. This ensures that only authorized users can initialize wallet addresses.
|
||||||
|
|
||||||
|
## Security Rules
|
||||||
|
|
||||||
|
### For Regular Users
|
||||||
|
- Can **only** initialize wallet addresses that they own
|
||||||
|
- The system verifies ownership by checking if the provided public address exists in one of the user's accounts
|
||||||
|
- If a user tries to initialize an address they don't own, they receive a `403 Forbidden` response
|
||||||
|
|
||||||
|
### For Admin Users
|
||||||
|
- Can initialize **any** wallet address in the system
|
||||||
|
- Admin status is determined by the `AdminUsers` environment variable
|
||||||
|
- All admin actions are logged for audit purposes
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
1. **User Authentication**: Endpoint requires valid JWT token
|
||||||
|
2. **Admin Check**: System checks if user is in the admin list via `IAdminConfigurationService`
|
||||||
|
3. **Ownership Verification**: For non-admin users, verifies address ownership via `IAccountService.GetAccountByKey()`
|
||||||
|
4. **Action Logging**: All operations are logged with user context
|
||||||
|
|
||||||
|
### Security Validation
|
||||||
|
```csharp
|
||||||
|
private async Task<bool> CanUserInitializeAddress(string userName, string publicAddress)
|
||||||
|
{
|
||||||
|
// Admin users can initialize any address
|
||||||
|
if (_adminService.IsUserAdmin(userName))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular users can only initialize their own addresses
|
||||||
|
var account = await _accountService.GetAccountByKey(publicAddress, true, false);
|
||||||
|
return account?.User?.Name == userName;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
### 401 Unauthorized
|
||||||
|
- Missing or invalid JWT token
|
||||||
|
- User not found in system
|
||||||
|
|
||||||
|
### 403 Forbidden
|
||||||
|
- Non-admin user trying to initialize address they don't own
|
||||||
|
- Message: "You don't have permission to initialize this wallet address. You can only initialize your own wallet addresses."
|
||||||
|
|
||||||
|
### 400 Bad Request
|
||||||
|
- Empty or null public address provided
|
||||||
|
|
||||||
|
### 500 Internal Server Error
|
||||||
|
- System error during wallet initialization
|
||||||
|
- Database connectivity issues
|
||||||
|
- External service failures
|
||||||
|
|
||||||
|
## Admin Configuration
|
||||||
|
|
||||||
|
Set admin users via environment variable:
|
||||||
|
```bash
|
||||||
|
AdminUsers=admin1,admin2,superuser
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit Logging
|
||||||
|
|
||||||
|
All operations are logged with appropriate context:
|
||||||
|
|
||||||
|
**Admin Operations:**
|
||||||
|
```
|
||||||
|
Admin user {UserName} initializing address {Address}
|
||||||
|
```
|
||||||
|
|
||||||
|
**User Operations:**
|
||||||
|
```
|
||||||
|
User {UserName} initializing their own address {Address}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security Violations:**
|
||||||
|
```
|
||||||
|
User {UserName} attempted to initialize address {Address} that doesn't belong to them
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Regular User - Own Address
|
||||||
|
```bash
|
||||||
|
POST /Trading/InitPrivyWallet
|
||||||
|
Authorization: Bearer {user-jwt-token}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
"0x1234567890123456789012345678901234567890"
|
||||||
|
```
|
||||||
|
**Result**: ✅ Success (if address belongs to user)
|
||||||
|
|
||||||
|
### Regular User - Other's Address
|
||||||
|
```bash
|
||||||
|
POST /Trading/InitPrivyWallet
|
||||||
|
Authorization: Bearer {user-jwt-token}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||||
|
```
|
||||||
|
**Result**: ❌ 403 Forbidden
|
||||||
|
|
||||||
|
### Admin User - Any Address
|
||||||
|
```bash
|
||||||
|
POST /Trading/InitPrivyWallet
|
||||||
|
Authorization: Bearer {admin-jwt-token}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||||
|
```
|
||||||
|
**Result**: ✅ Success (admin can initialize any address)
|
||||||
|
|
||||||
|
## Security Benefits
|
||||||
|
|
||||||
|
1. **Prevents Unauthorized Access**: Users cannot initialize wallets they don't own
|
||||||
|
2. **Admin Oversight**: Admins can manage any wallet for system administration
|
||||||
|
3. **Audit Trail**: All actions are logged for compliance and security monitoring
|
||||||
|
4. **Clear Authorization**: Explicit permission checks with meaningful error messages
|
||||||
|
5. **Secure Configuration**: Admin privileges configured via environment variables, not API calls
|
||||||
@@ -81,5 +81,6 @@
|
|||||||
"WorkerBundleBacktest": false,
|
"WorkerBundleBacktest": false,
|
||||||
"WorkerBalancesTracking": false,
|
"WorkerBalancesTracking": false,
|
||||||
"WorkerNotifyBundleBacktest": false,
|
"WorkerNotifyBundleBacktest": false,
|
||||||
|
"AdminUsers": "",
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
55
src/Managing.Application/Shared/AdminConfigurationService.cs
Normal file
55
src/Managing.Application/Shared/AdminConfigurationService.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Managing.Application.Shared;
|
||||||
|
|
||||||
|
public interface IAdminConfigurationService
|
||||||
|
{
|
||||||
|
bool IsUserAdmin(string userName);
|
||||||
|
List<string> GetAdminUserNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AdminConfigurationService : IAdminConfigurationService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ILogger<AdminConfigurationService> _logger;
|
||||||
|
|
||||||
|
public AdminConfigurationService(IConfiguration configuration, ILogger<AdminConfigurationService> logger)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsUserAdmin(string userName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(userName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var adminUserNames = GetAdminUserNames();
|
||||||
|
var isAdmin = adminUserNames.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (isAdmin)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("User {UserName} has admin privileges", userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetAdminUserNames()
|
||||||
|
{
|
||||||
|
var adminUsers = _configuration["AdminUsers"];
|
||||||
|
if (string.IsNullOrEmpty(adminUsers))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No admin users configured. Set AdminUsers environment variable.");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return adminUsers.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(u => u.Trim())
|
||||||
|
.Where(u => !string.IsNullOrEmpty(u))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -288,6 +288,9 @@ public static class ApiBootstrap
|
|||||||
services.AddSingleton<IMessengerService, MessengerService>();
|
services.AddSingleton<IMessengerService, MessengerService>();
|
||||||
services.AddSingleton<IDiscordService, DiscordService>();
|
services.AddSingleton<IDiscordService, DiscordService>();
|
||||||
|
|
||||||
|
// Admin services
|
||||||
|
services.AddSingleton<IAdminConfigurationService, AdminConfigurationService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user