Updated to latest mooncore version. Cleaned up some crud controllers and replaced DataTable with the new DataGrid component
This commit is contained in:
@@ -1,12 +1,10 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Extended.Models;
|
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
using Moonlight.ApiServer.Mappers;
|
||||||
using Moonlight.ApiServer.Services;
|
using Moonlight.ApiServer.Services;
|
||||||
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||||
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||||
@@ -28,69 +26,82 @@ public class ApiKeysController : Controller
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Policy = "permissions:admin.apikeys.get")]
|
[Authorize(Policy = "permissions:admin.apikeys.get")]
|
||||||
public async Task<IPagedData<ApiKeyResponse>> Get([FromQuery] PagedOptions options)
|
public async Task<ActionResult<ICountedData<ApiKeyResponse>>> Get(
|
||||||
|
[FromQuery] int startIndex,
|
||||||
|
[FromQuery] int count,
|
||||||
|
[FromQuery] string? orderBy,
|
||||||
|
[FromQuery] string? filter,
|
||||||
|
[FromQuery] string orderByDir = "asc"
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var count = await ApiKeyRepository.Get().CountAsync();
|
if (count > 100)
|
||||||
|
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
|
||||||
|
|
||||||
var apiKeys = await ApiKeyRepository
|
IQueryable<ApiKey> query = ApiKeyRepository.Get();
|
||||||
.Get()
|
|
||||||
.OrderBy(x => x.Id)
|
query = orderBy switch
|
||||||
.Skip(options.Page * options.PageSize)
|
{
|
||||||
.Take(options.PageSize)
|
nameof(ApiKey.Id) => orderByDir == "desc"
|
||||||
|
? query.OrderByDescending(x => x.Id)
|
||||||
|
: query.OrderBy(x => x.Id),
|
||||||
|
|
||||||
|
nameof(ApiKey.ExpiresAt) => orderByDir == "desc"
|
||||||
|
? query.OrderByDescending(x => x.ExpiresAt)
|
||||||
|
: query.OrderBy(x => x.ExpiresAt),
|
||||||
|
|
||||||
|
nameof(ApiKey.CreatedAt) => orderByDir == "desc"
|
||||||
|
? query.OrderByDescending(x => x.CreatedAt)
|
||||||
|
: query.OrderBy(x => x.CreatedAt),
|
||||||
|
|
||||||
|
_ => query.OrderBy(x => x.Id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(filter))
|
||||||
|
{
|
||||||
|
query = query.Where(x =>
|
||||||
|
EF.Functions.ILike(x.Description, $"%{filter}%")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync();
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.Skip(startIndex)
|
||||||
|
.Take(count)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToResponse()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var mappedApiKey = apiKeys
|
return new CountedData<ApiKeyResponse>()
|
||||||
.Select(x => new ApiKeyResponse()
|
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
Permissions = x.Permissions,
|
|
||||||
Description = x.Description,
|
|
||||||
ExpiresAt = x.ExpiresAt
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new PagedData<ApiKeyResponse>()
|
|
||||||
{
|
{
|
||||||
CurrentPage = options.Page,
|
Items = items,
|
||||||
Items = mappedApiKey,
|
TotalCount = totalCount
|
||||||
PageSize = options.PageSize,
|
|
||||||
TotalItems = count,
|
|
||||||
TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.apikeys.get")]
|
[Authorize(Policy = "permissions:admin.apikeys.get")]
|
||||||
public async Task<ApiKeyResponse> GetSingle(int id)
|
public async Task<ActionResult<ApiKeyResponse>> GetSingle(int id)
|
||||||
{
|
{
|
||||||
var apiKey = await ApiKeyRepository
|
var apiKey = await ApiKeyRepository
|
||||||
.Get()
|
.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToResponse()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (apiKey == null)
|
if (apiKey == null)
|
||||||
throw new HttpApiException("No api key with that id found", 404);
|
return Problem("No api key with that id found", statusCode: 404);
|
||||||
|
|
||||||
return new ApiKeyResponse()
|
return apiKey;
|
||||||
{
|
|
||||||
Id = apiKey.Id,
|
|
||||||
Permissions = apiKey.Permissions,
|
|
||||||
Description = apiKey.Description,
|
|
||||||
ExpiresAt = apiKey.ExpiresAt
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Policy = "permissions:admin.apikeys.create")]
|
[Authorize(Policy = "permissions:admin.apikeys.create")]
|
||||||
public async Task<CreateApiKeyResponse> Create([FromBody] CreateApiKeyRequest request)
|
public async Task<CreateApiKeyResponse> Create([FromBody] CreateApiKeyRequest request)
|
||||||
{
|
{
|
||||||
var apiKey = new ApiKey()
|
var apiKey = ApiKeyMapper.ToApiKey(request);
|
||||||
{
|
|
||||||
Description = request.Description,
|
|
||||||
Permissions = request.Permissions,
|
|
||||||
ExpiresAt = request.ExpiresAt
|
|
||||||
};
|
|
||||||
|
|
||||||
var finalApiKey = await ApiKeyRepository.Add(apiKey);
|
var finalApiKey = await ApiKeyRepository.AddAsync(apiKey);
|
||||||
|
|
||||||
var response = new CreateApiKeyResponse
|
var response = new CreateApiKeyResponse
|
||||||
{
|
{
|
||||||
@@ -104,41 +115,36 @@ public class ApiKeysController : Controller
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{id}")]
|
[HttpPatch("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.apikeys.update")]
|
[Authorize(Policy = "permissions:admin.apikeys.update")]
|
||||||
public async Task<ApiKeyResponse> Update([FromRoute] int id, [FromBody] UpdateApiKeyRequest request)
|
public async Task<ActionResult<ApiKeyResponse>> Update([FromRoute] int id, [FromBody] UpdateApiKeyRequest request)
|
||||||
{
|
{
|
||||||
var apiKey = await ApiKeyRepository
|
var apiKey = await ApiKeyRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (apiKey == null)
|
if (apiKey == null)
|
||||||
throw new HttpApiException("No api key with that id found", 404);
|
return Problem("No api key with that id found", statusCode: 404);
|
||||||
|
|
||||||
apiKey.Description = request.Description;
|
ApiKeyMapper.Merge(apiKey, request);
|
||||||
|
|
||||||
await ApiKeyRepository.Update(apiKey);
|
await ApiKeyRepository.UpdateAsync(apiKey);
|
||||||
|
|
||||||
return new ApiKeyResponse()
|
return ApiKeyMapper.ToResponse(apiKey);
|
||||||
{
|
|
||||||
Id = apiKey.Id,
|
|
||||||
Description = apiKey.Description,
|
|
||||||
Permissions = apiKey.Permissions,
|
|
||||||
ExpiresAt = apiKey.ExpiresAt
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.apikeys.delete")]
|
[Authorize(Policy = "permissions:admin.apikeys.delete")]
|
||||||
public async Task Delete([FromRoute] int id)
|
public async Task<ActionResult> Delete([FromRoute] int id)
|
||||||
{
|
{
|
||||||
var apiKey = await ApiKeyRepository
|
var apiKey = await ApiKeyRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (apiKey == null)
|
if (apiKey == null)
|
||||||
throw new HttpApiException("No api key with that id found", 404);
|
return Problem("No api key with that id found", statusCode: 404);
|
||||||
|
|
||||||
await ApiKeyRepository.Remove(apiKey);
|
await ApiKeyRepository.RemoveAsync(apiKey);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,65 +26,96 @@ public class ThemesController : Controller
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
|
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
|
||||||
public async Task<PagedData<ThemeResponse>> Get([FromQuery] PagedOptions options)
|
public async Task<ActionResult<ICountedData<ThemeResponse>>> Get(
|
||||||
|
[FromQuery] int startIndex,
|
||||||
|
[FromQuery] int count,
|
||||||
|
[FromQuery] string? orderBy,
|
||||||
|
[FromQuery] string? filter,
|
||||||
|
[FromQuery] string orderByDir = "asc"
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var count = await ThemeRepository.Get().CountAsync();
|
if (count > 100)
|
||||||
|
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
|
||||||
|
|
||||||
var items = await ThemeRepository
|
IQueryable<Theme> query = ThemeRepository.Get();
|
||||||
.Get()
|
|
||||||
.Skip(options.Page * options.PageSize)
|
query = orderBy switch
|
||||||
.Take(options.PageSize)
|
{
|
||||||
|
nameof(Theme.Id) => orderByDir == "desc"
|
||||||
|
? query.OrderByDescending(x => x.Id)
|
||||||
|
: query.OrderBy(x => x.Id),
|
||||||
|
|
||||||
|
nameof(Theme.Name) => orderByDir == "desc"
|
||||||
|
? query.OrderByDescending(x => x.Name)
|
||||||
|
: query.OrderBy(x => x.Name),
|
||||||
|
|
||||||
|
nameof(Theme.Version) => orderByDir == "desc"
|
||||||
|
? query.OrderByDescending(x => x.Version)
|
||||||
|
: query.OrderBy(x => x.Version),
|
||||||
|
|
||||||
|
_ => query.OrderBy(x => x.Id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(filter))
|
||||||
|
{
|
||||||
|
query = query.Where(x =>
|
||||||
|
EF.Functions.ILike(x.Name, $"%{filter}%")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync();
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.Skip(startIndex)
|
||||||
|
.Take(count)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToResponse()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var mappedItems = items
|
return new CountedData<ThemeResponse>()
|
||||||
.Select(ThemeMapper.ToResponse)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new PagedData<ThemeResponse>()
|
|
||||||
{
|
{
|
||||||
CurrentPage = options.Page,
|
Items = items,
|
||||||
Items = mappedItems,
|
TotalCount = totalCount
|
||||||
PageSize = options.PageSize,
|
|
||||||
TotalItems = count,
|
|
||||||
TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
|
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
|
||||||
public async Task<ThemeResponse> GetSingle([FromRoute] int id)
|
public async Task<ActionResult<ThemeResponse>> GetSingle([FromRoute] int id)
|
||||||
{
|
{
|
||||||
var theme = await ThemeRepository
|
var theme = await ThemeRepository
|
||||||
.Get()
|
.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToResponse()
|
||||||
.FirstOrDefaultAsync(t => t.Id == id);
|
.FirstOrDefaultAsync(t => t.Id == id);
|
||||||
|
|
||||||
if (theme == null)
|
if (theme == null)
|
||||||
throw new HttpApiException("Theme with this id not found", 404);
|
return Problem("Theme with this id not found", statusCode: 404);
|
||||||
|
|
||||||
return ThemeMapper.ToResponse(theme);
|
return theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
|
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
|
||||||
public async Task<ThemeResponse> Create([FromBody] CreateThemeRequest request)
|
public async Task<ActionResult<ThemeResponse>> Create([FromBody] CreateThemeRequest request)
|
||||||
{
|
{
|
||||||
var theme = ThemeMapper.ToTheme(request);
|
var theme = ThemeMapper.ToTheme(request);
|
||||||
|
|
||||||
var finalTheme = await ThemeRepository.Add(theme);
|
var finalTheme = await ThemeRepository.AddAsync(theme);
|
||||||
|
|
||||||
return ThemeMapper.ToResponse(finalTheme);
|
return ThemeMapper.ToResponse(finalTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{id:int}")]
|
[HttpPatch("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
|
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
|
||||||
public async Task<ThemeResponse> Update([FromRoute] int id, [FromBody] UpdateThemeRequest request)
|
public async Task<ActionResult<ThemeResponse>> Update([FromRoute] int id, [FromBody] UpdateThemeRequest request)
|
||||||
{
|
{
|
||||||
var theme = await ThemeRepository
|
var theme = await ThemeRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(t => t.Id == id);
|
.FirstOrDefaultAsync(t => t.Id == id);
|
||||||
|
|
||||||
if (theme == null)
|
if (theme == null)
|
||||||
throw new HttpApiException("Theme with this id not found", 404);
|
return Problem("Theme with this id not found", statusCode: 404);
|
||||||
|
|
||||||
// Disable all other enabled themes if we are enabling the current theme.
|
// Disable all other enabled themes if we are enabling the current theme.
|
||||||
// This ensures only one theme is enabled at the time
|
// This ensures only one theme is enabled at the time
|
||||||
@@ -98,29 +129,28 @@ public class ThemesController : Controller
|
|||||||
foreach (var otherTheme in otherThemes)
|
foreach (var otherTheme in otherThemes)
|
||||||
otherTheme.IsEnabled = false;
|
otherTheme.IsEnabled = false;
|
||||||
|
|
||||||
await ThemeRepository.RunTransaction(set =>
|
await ThemeRepository.RunTransactionAsync(set => { set.UpdateRange(otherThemes); });
|
||||||
{
|
|
||||||
set.UpdateRange(otherThemes);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeMapper.Merge(theme, request);
|
ThemeMapper.Merge(theme, request);
|
||||||
await ThemeRepository.Update(theme);
|
|
||||||
|
await ThemeRepository.UpdateAsync(theme);
|
||||||
|
|
||||||
return ThemeMapper.ToResponse(theme);
|
return ThemeMapper.ToResponse(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
[HttpDelete("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
|
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
|
||||||
public async Task Delete([FromRoute] int id)
|
public async Task<ActionResult> Delete([FromRoute] int id)
|
||||||
{
|
{
|
||||||
var theme = await ThemeRepository
|
var theme = await ThemeRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (theme == null)
|
if (theme == null)
|
||||||
throw new HttpApiException("Theme with this id not found", 404);
|
return Problem("Theme with this id not found", statusCode: 404);
|
||||||
|
|
||||||
await ThemeRepository.Remove(theme);
|
await ThemeRepository.RemoveAsync(theme);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -6,10 +5,10 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Extended.Helpers;
|
using MoonCore.Extended.Helpers;
|
||||||
using MoonCore.Extended.Models;
|
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
using Moonlight.ApiServer.Services;
|
using Moonlight.ApiServer.Services;
|
||||||
|
using Moonlight.ApiServer.Mappers;
|
||||||
using Moonlight.Shared.Http.Requests.Admin.Users;
|
using Moonlight.Shared.Http.Requests.Admin.Users;
|
||||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||||
|
|
||||||
@@ -28,60 +27,78 @@ public class UsersController : Controller
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Policy = "permissions:admin.users.get")]
|
[Authorize(Policy = "permissions:admin.users.get")]
|
||||||
public async Task<IPagedData<UserResponse>> Get([FromQuery] PagedOptions options)
|
public async Task<ActionResult<ICountedData<UserResponse>>> Get(
|
||||||
|
[FromQuery] int startIndex,
|
||||||
|
[FromQuery] int count,
|
||||||
|
[FromQuery] string? orderBy,
|
||||||
|
[FromQuery] string? filter,
|
||||||
|
[FromQuery] string orderByDir = "asc"
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var count = await UserRepository.Get().CountAsync();
|
if (count > 100)
|
||||||
|
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
|
||||||
|
|
||||||
var users = await UserRepository
|
IQueryable<User> query = UserRepository.Get();
|
||||||
.Get()
|
|
||||||
.OrderBy(x => x.Id)
|
query = orderBy switch
|
||||||
.Skip(options.Page * options.PageSize)
|
{
|
||||||
.Take(options.PageSize)
|
nameof(Database.Entities.User.Id) => orderByDir == "desc"
|
||||||
|
? query.OrderByDescending(x => x.Id)
|
||||||
|
: query.OrderBy(x => x.Id),
|
||||||
|
|
||||||
|
nameof(Database.Entities.User.Username) => orderByDir == "desc"
|
||||||
|
? query.OrderByDescending(x => x.Username)
|
||||||
|
: query.OrderBy(x => x.Username),
|
||||||
|
|
||||||
|
nameof(Database.Entities.User.Email) => orderByDir == "desc"
|
||||||
|
? query.OrderByDescending(x => x.Email)
|
||||||
|
: query.OrderBy(x => x.Email),
|
||||||
|
|
||||||
|
_ => query.OrderBy(x => x.Id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(filter))
|
||||||
|
{
|
||||||
|
query = query.Where(x =>
|
||||||
|
EF.Functions.ILike(x.Username, $"%{filter}%") ||
|
||||||
|
EF.Functions.ILike(x.Email, $"%{filter}%")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync();
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.Skip(startIndex)
|
||||||
|
.Take(count)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToResponse()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var mappedUsers = users
|
return new CountedData<UserResponse>()
|
||||||
.Select(x => new UserResponse()
|
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
Email = x.Email,
|
|
||||||
Username = x.Username,
|
|
||||||
Permissions = x.Permissions
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new PagedData<UserResponse>()
|
|
||||||
{
|
{
|
||||||
CurrentPage = options.Page,
|
Items = items,
|
||||||
Items = mappedUsers,
|
TotalCount = totalCount
|
||||||
PageSize = options.PageSize,
|
|
||||||
TotalItems = count,
|
|
||||||
TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[Authorize(Policy = "permissions:admin.users.get")]
|
[Authorize(Policy = "permissions:admin.users.get")]
|
||||||
public async Task<UserResponse> GetSingle(int id)
|
public async Task<ActionResult<UserResponse>> GetSingle(int id)
|
||||||
{
|
{
|
||||||
var user = await UserRepository
|
var user = await UserRepository
|
||||||
.Get()
|
.Get()
|
||||||
|
.ProjectToResponse()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new HttpApiException("No user with that id found", 404);
|
return Problem("No user with that id found", statusCode: 404);
|
||||||
|
|
||||||
return new UserResponse()
|
return user;
|
||||||
{
|
|
||||||
Id = user.Id,
|
|
||||||
Email = user.Email,
|
|
||||||
Username = user.Username,
|
|
||||||
Permissions = user.Permissions
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Policy = "permissions:admin.users.create")]
|
[Authorize(Policy = "permissions:admin.users.create")]
|
||||||
public async Task<UserResponse> Create([FromBody] CreateUserRequest request)
|
public async Task<ActionResult<UserResponse>> Create([FromBody] CreateUserRequest request)
|
||||||
{
|
{
|
||||||
// Reformat values
|
// Reformat values
|
||||||
request.Username = request.Username.ToLower().Trim();
|
request.Username = request.Username.ToLower().Trim();
|
||||||
@@ -89,10 +106,10 @@ public class UsersController : Controller
|
|||||||
|
|
||||||
// Check for users with the same values
|
// Check for users with the same values
|
||||||
if (UserRepository.Get().Any(x => x.Username == request.Username))
|
if (UserRepository.Get().Any(x => x.Username == request.Username))
|
||||||
throw new HttpApiException("A user with that username already exists", 400);
|
return Problem("A user with that username already exists", statusCode: 400);
|
||||||
|
|
||||||
if (UserRepository.Get().Any(x => x.Email == request.Email))
|
if (UserRepository.Get().Any(x => x.Email == request.Email))
|
||||||
throw new HttpApiException("A user with that email address already exists", 400);
|
return Problem("A user with that email address already exists", statusCode: 400);
|
||||||
|
|
||||||
var hashedPassword = HashHelper.Hash(request.Password);
|
var hashedPassword = HashHelper.Hash(request.Password);
|
||||||
|
|
||||||
@@ -104,27 +121,21 @@ public class UsersController : Controller
|
|||||||
Permissions = request.Permissions
|
Permissions = request.Permissions
|
||||||
};
|
};
|
||||||
|
|
||||||
var finalUser = await UserRepository.Add(user);
|
var finalUser = await UserRepository.AddAsync(user);
|
||||||
|
|
||||||
return new UserResponse()
|
return UserMapper.ToResponse(finalUser);
|
||||||
{
|
|
||||||
Id = finalUser.Id,
|
|
||||||
Email = finalUser.Email,
|
|
||||||
Username = finalUser.Username,
|
|
||||||
Permissions = finalUser.Permissions
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{id}")]
|
[HttpPatch("{id}")]
|
||||||
[Authorize(Policy = "permissions:admin.users.update")]
|
[Authorize(Policy = "permissions:admin.users.update")]
|
||||||
public async Task<UserResponse> Update([FromRoute] int id, [FromBody] UpdateUserRequest request)
|
public async Task<ActionResult<UserResponse>> Update([FromRoute] int id, [FromBody] UpdateUserRequest request)
|
||||||
{
|
{
|
||||||
var user = await UserRepository
|
var user = await UserRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new HttpApiException("No user with that id found", 404);
|
return Problem("No user with that id found", statusCode: 404);
|
||||||
|
|
||||||
// Reformat values
|
// Reformat values
|
||||||
request.Username = request.Username.ToLower().Trim();
|
request.Username = request.Username.ToLower().Trim();
|
||||||
@@ -132,10 +143,10 @@ public class UsersController : Controller
|
|||||||
|
|
||||||
// Check for users with the same values
|
// Check for users with the same values
|
||||||
if (UserRepository.Get().Any(x => x.Username == request.Username && x.Id != user.Id))
|
if (UserRepository.Get().Any(x => x.Username == request.Username && x.Id != user.Id))
|
||||||
throw new HttpApiException("A user with that username already exists", 400);
|
return Problem("Another user with that username already exists", statusCode: 400);
|
||||||
|
|
||||||
if (UserRepository.Get().Any(x => x.Email == request.Email && x.Id != user.Id))
|
if (UserRepository.Get().Any(x => x.Email == request.Email && x.Id != user.Id))
|
||||||
throw new HttpApiException("A user with that email address already exists", 400);
|
return Problem("Another user with that email address already exists", statusCode: 400);
|
||||||
|
|
||||||
// Perform hashing the password if required
|
// Perform hashing the password if required
|
||||||
if (!string.IsNullOrEmpty(request.Password))
|
if (!string.IsNullOrEmpty(request.Password))
|
||||||
@@ -153,27 +164,21 @@ public class UsersController : Controller
|
|||||||
user.Email = request.Email;
|
user.Email = request.Email;
|
||||||
user.Username = request.Username;
|
user.Username = request.Username;
|
||||||
|
|
||||||
await UserRepository.Update(user);
|
await UserRepository.UpdateAsync(user);
|
||||||
|
|
||||||
return new UserResponse()
|
return UserMapper.ToResponse(user);
|
||||||
{
|
|
||||||
Id = user.Id,
|
|
||||||
Email = user.Email,
|
|
||||||
Username = user.Username,
|
|
||||||
Permissions = user.Permissions
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
[Authorize(Policy = "permissions:admin.users.delete")]
|
[Authorize(Policy = "permissions:admin.users.delete")]
|
||||||
public async Task Delete([FromRoute] int id, [FromQuery] bool force = false)
|
public async Task<ActionResult> Delete([FromRoute] int id, [FromQuery] bool force = false)
|
||||||
{
|
{
|
||||||
var user = await UserRepository
|
var user = await UserRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new HttpApiException("No user with that id found", 404);
|
return Problem("No user with that id found", statusCode: 404);
|
||||||
|
|
||||||
var deletionService = HttpContext.RequestServices.GetRequiredService<UserDeletionService>();
|
var deletionService = HttpContext.RequestServices.GetRequiredService<UserDeletionService>();
|
||||||
|
|
||||||
@@ -182,9 +187,10 @@ public class UsersController : Controller
|
|||||||
var validationResult = await deletionService.Validate(user);
|
var validationResult = await deletionService.Validate(user);
|
||||||
|
|
||||||
if (!validationResult.IsAllowed)
|
if (!validationResult.IsAllowed)
|
||||||
throw new HttpApiException($"Unable to delete user", 400, validationResult.Reason);
|
return Problem("Unable to delete user", statusCode: 400, title: validationResult.Reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
await deletionService.Delete(user, force);
|
await deletionService.Delete(user, force);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +180,7 @@ public class LocalAuthController : Controller
|
|||||||
Permissions = permissions
|
Permissions = permissions
|
||||||
};
|
};
|
||||||
|
|
||||||
var finalUser = await UserRepository.Add(user);
|
var finalUser = await UserRepository.AddAsync(user);
|
||||||
|
|
||||||
return finalUser;
|
return finalUser;
|
||||||
}
|
}
|
||||||
|
|||||||
19
Moonlight.ApiServer/Mappers/ApiKeyMapper.cs
Normal file
19
Moonlight.ApiServer/Mappers/ApiKeyMapper.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||||
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Mappers;
|
||||||
|
|
||||||
|
[Mapper]
|
||||||
|
public static partial class ApiKeyMapper
|
||||||
|
{
|
||||||
|
// Mappers
|
||||||
|
public static partial ApiKeyResponse ToResponse(ApiKey apiKey);
|
||||||
|
public static partial ApiKey ToApiKey(CreateApiKeyRequest request);
|
||||||
|
public static partial void Merge([MappingTarget] ApiKey apiKey, UpdateApiKeyRequest request);
|
||||||
|
|
||||||
|
// EF Relations
|
||||||
|
|
||||||
|
public static partial IQueryable<ApiKeyResponse> ProjectToResponse(this IQueryable<ApiKey> apiKeys);
|
||||||
|
}
|
||||||
@@ -8,7 +8,12 @@ namespace Moonlight.ApiServer.Mappers;
|
|||||||
[Mapper]
|
[Mapper]
|
||||||
public static partial class ThemeMapper
|
public static partial class ThemeMapper
|
||||||
{
|
{
|
||||||
|
// Mappers
|
||||||
public static partial ThemeResponse ToResponse(Theme theme);
|
public static partial ThemeResponse ToResponse(Theme theme);
|
||||||
public static partial Theme ToTheme(CreateThemeRequest request);
|
public static partial Theme ToTheme(CreateThemeRequest request);
|
||||||
public static partial void Merge([MappingTarget] Theme theme, UpdateThemeRequest request);
|
public static partial void Merge([MappingTarget] Theme theme, UpdateThemeRequest request);
|
||||||
|
|
||||||
|
// EF Relations
|
||||||
|
|
||||||
|
public static partial IQueryable<ThemeResponse> ProjectToResponse(this IQueryable<Theme> themes);
|
||||||
}
|
}
|
||||||
15
Moonlight.ApiServer/Mappers/UserMapper.cs
Normal file
15
Moonlight.ApiServer/Mappers/UserMapper.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||||
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Mappers;
|
||||||
|
|
||||||
|
[Mapper]
|
||||||
|
public static partial class UserMapper
|
||||||
|
{
|
||||||
|
// Mappers
|
||||||
|
public static partial UserResponse ToResponse(User user);
|
||||||
|
|
||||||
|
// EF Relations
|
||||||
|
public static partial IQueryable<UserResponse> ProjectToResponse(this IQueryable<User> users);
|
||||||
|
}
|
||||||
@@ -25,9 +25,10 @@
|
|||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.20"/>
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.20"/>
|
||||||
<PackageReference Include="Hangfire.Core" Version="1.8.20"/>
|
<PackageReference Include="Hangfire.Core" Version="1.8.20"/>
|
||||||
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
|
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.9" />
|
||||||
<PackageReference Include="MoonCore" Version="1.9.7" />
|
<PackageReference Include="MoonCore" Version="1.9.9" />
|
||||||
<PackageReference Include="MoonCore.Extended" Version="1.3.7" />
|
<PackageReference Include="MoonCore.Extended" Version="1.3.8" />
|
||||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2"/>
|
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2"/>
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.12.0-beta.1"/>
|
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.12.0-beta.1"/>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class UserAuthService
|
|||||||
permissions = ["*"];
|
permissions = ["*"];
|
||||||
}
|
}
|
||||||
|
|
||||||
user = await UserRepository.Add(new User()
|
user = await UserRepository.AddAsync(new User()
|
||||||
{
|
{
|
||||||
Email = email,
|
Email = email,
|
||||||
TokenValidTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1),
|
TokenValidTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1),
|
||||||
@@ -94,7 +94,7 @@ public class UserAuthService
|
|||||||
if (user.Username != username)
|
if (user.Username != username)
|
||||||
{
|
{
|
||||||
user.Username = username;
|
user.Username = username;
|
||||||
await UserRepository.Update(user);
|
await UserRepository.UpdateAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich claims with required metadata
|
// Enrich claims with required metadata
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ public class UserDeletionService
|
|||||||
public async Task Delete(User user, bool force)
|
public async Task Delete(User user, bool force)
|
||||||
{
|
{
|
||||||
foreach (var handler in Handlers)
|
foreach (var handler in Handlers)
|
||||||
await Delete(user, force);
|
await handler.Delete(user, force);
|
||||||
|
|
||||||
await UserRepository.Remove(user);
|
await UserRepository.RemoveAsync(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,9 +24,9 @@
|
|||||||
<PackageReference Include="Blazor-ApexCharts" Version="6.0.2" />
|
<PackageReference Include="Blazor-ApexCharts" Version="6.0.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
|
||||||
<PackageReference Include="MoonCore" Version="1.9.7" />
|
<PackageReference Include="MoonCore" Version="1.9.9" />
|
||||||
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" />
|
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" />
|
||||||
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.1.9" />
|
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.2.2" />
|
||||||
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
|
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -17,21 +17,14 @@ public class ThemeService
|
|||||||
ApiClient = apiClient;
|
ApiClient = apiClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PagedData<ThemeResponse>> Get(int page, int pageSize)
|
public async Task<ThemeResponse> GetAsync(int id)
|
||||||
{
|
|
||||||
return await ApiClient.GetJson<PagedData<ThemeResponse>>(
|
|
||||||
$"api/admin/system/customisation/themes?page={page}&pageSize={pageSize}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ThemeResponse> Get(int id)
|
|
||||||
{
|
{
|
||||||
return await ApiClient.GetJson<ThemeResponse>(
|
return await ApiClient.GetJson<ThemeResponse>(
|
||||||
$"api/admin/system/customisation/themes/{id}"
|
$"api/admin/system/customisation/themes/{id}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ThemeResponse> Create(CreateThemeRequest request)
|
public async Task<ThemeResponse> CreateAsync(CreateThemeRequest request)
|
||||||
{
|
{
|
||||||
return await ApiClient.PostJson<ThemeResponse>(
|
return await ApiClient.PostJson<ThemeResponse>(
|
||||||
"api/admin/system/customisation/themes",
|
"api/admin/system/customisation/themes",
|
||||||
@@ -39,7 +32,7 @@ public class ThemeService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ThemeResponse> Update(int id, UpdateThemeRequest request)
|
public async Task<ThemeResponse> UpdateAsync(int id, UpdateThemeRequest request)
|
||||||
{
|
{
|
||||||
return await ApiClient.PatchJson<ThemeResponse>(
|
return await ApiClient.PatchJson<ThemeResponse>(
|
||||||
$"api/admin/system/customisation/themes/{id}",
|
$"api/admin/system/customisation/themes/{id}",
|
||||||
@@ -47,7 +40,7 @@ public class ThemeService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Delete(int id)
|
public async Task DeleteAsync(int id)
|
||||||
{
|
{
|
||||||
await ApiClient.Delete(
|
await ApiClient.Delete(
|
||||||
$"api/admin/system/customisation/themes/{id}"
|
$"api/admin/system/customisation/themes/{id}"
|
||||||
|
|||||||
@@ -1,518 +0,0 @@
|
|||||||
!bg-base-100
|
|
||||||
!border-base-content/40
|
|
||||||
!border-none
|
|
||||||
!flex
|
|
||||||
!font-medium
|
|
||||||
!font-semibold
|
|
||||||
!h-2.5
|
|
||||||
!justify-between
|
|
||||||
!me-1.5
|
|
||||||
!ms-auto
|
|
||||||
!px-2.5
|
|
||||||
!rounded-full
|
|
||||||
!text-sm
|
|
||||||
!w-2.5
|
|
||||||
*:[grid-area:1/1]
|
|
||||||
*:first:rounded-tl-lg
|
|
||||||
*:last:rounded-tr-lg
|
|
||||||
-left-4
|
|
||||||
-ml-4
|
|
||||||
-translate-x-full
|
|
||||||
-translate-y-1/2
|
|
||||||
absolute
|
|
||||||
accordion
|
|
||||||
accordion-bordered
|
|
||||||
accordion-toggle
|
|
||||||
active
|
|
||||||
active-tab:bg-primary
|
|
||||||
active-tab:text-base-content
|
|
||||||
advance-select-menu
|
|
||||||
advance-select-option
|
|
||||||
advance-select-tag
|
|
||||||
advance-select-toggle
|
|
||||||
alert
|
|
||||||
alert-error
|
|
||||||
alert-outline
|
|
||||||
alert-soft
|
|
||||||
align-bottom
|
|
||||||
align-middle
|
|
||||||
animate-bounce
|
|
||||||
animate-ping
|
|
||||||
aria-[current='page']:text-bg-soft-primary
|
|
||||||
avatar
|
|
||||||
avatar-away-bottom
|
|
||||||
avatar-away-top
|
|
||||||
avatar-busy-bottom
|
|
||||||
avatar-busy-top
|
|
||||||
avatar-offline-bottom
|
|
||||||
avatar-offline-top
|
|
||||||
avatar-online-bottom
|
|
||||||
avatar-online-top
|
|
||||||
avatar-placeholder
|
|
||||||
badge
|
|
||||||
badge-error
|
|
||||||
badge-info
|
|
||||||
badge-outline
|
|
||||||
badge-primary
|
|
||||||
badge-soft
|
|
||||||
badge-success
|
|
||||||
bg-background
|
|
||||||
bg-background/60
|
|
||||||
bg-base-100
|
|
||||||
bg-base-150
|
|
||||||
bg-base-200
|
|
||||||
bg-base-200/50
|
|
||||||
bg-base-300
|
|
||||||
bg-base-300/45
|
|
||||||
bg-base-300/50
|
|
||||||
bg-base-300/60
|
|
||||||
bg-error
|
|
||||||
bg-info
|
|
||||||
bg-primary
|
|
||||||
bg-primary/5
|
|
||||||
bg-success
|
|
||||||
bg-transparent
|
|
||||||
bg-warning
|
|
||||||
block
|
|
||||||
blur
|
|
||||||
border
|
|
||||||
border-0
|
|
||||||
border-2
|
|
||||||
border-b
|
|
||||||
border-base-content
|
|
||||||
border-base-content/20
|
|
||||||
border-base-content/25
|
|
||||||
border-base-content/40
|
|
||||||
border-base-content/5
|
|
||||||
border-dashed
|
|
||||||
border-t
|
|
||||||
border-transparent
|
|
||||||
bottom-0
|
|
||||||
bottom-full
|
|
||||||
break-words
|
|
||||||
btn
|
|
||||||
btn-accent
|
|
||||||
btn-active
|
|
||||||
btn-circle
|
|
||||||
btn-disabled
|
|
||||||
btn-error
|
|
||||||
btn-info
|
|
||||||
btn-outline
|
|
||||||
btn-primary
|
|
||||||
btn-secondary
|
|
||||||
btn-sm
|
|
||||||
btn-soft
|
|
||||||
btn-square
|
|
||||||
btn-success
|
|
||||||
btn-text
|
|
||||||
btn-warning
|
|
||||||
card
|
|
||||||
card-alert
|
|
||||||
card-body
|
|
||||||
card-border
|
|
||||||
card-footer
|
|
||||||
card-header
|
|
||||||
card-title
|
|
||||||
carousel
|
|
||||||
carousel-body
|
|
||||||
carousel-next
|
|
||||||
carousel-prev
|
|
||||||
carousel-slide
|
|
||||||
chat
|
|
||||||
chat-avatar
|
|
||||||
chat-bubble
|
|
||||||
chat-footer
|
|
||||||
chat-header
|
|
||||||
chat-receiver
|
|
||||||
chat-sender
|
|
||||||
checkbox
|
|
||||||
checkbox-primary
|
|
||||||
checkbox-xs
|
|
||||||
col-span-1
|
|
||||||
collapse
|
|
||||||
combo-box-selected:block
|
|
||||||
combo-box-selected:dropdown-active
|
|
||||||
complete
|
|
||||||
container
|
|
||||||
contents
|
|
||||||
cursor-default
|
|
||||||
cursor-not-allowed
|
|
||||||
cursor-pointer
|
|
||||||
diff
|
|
||||||
disabled
|
|
||||||
divide-base-150/60
|
|
||||||
divide-y
|
|
||||||
drop-shadow
|
|
||||||
dropdown
|
|
||||||
dropdown-disabled
|
|
||||||
dropdown-item
|
|
||||||
dropdown-menu
|
|
||||||
dropdown-open:opacity-100
|
|
||||||
dropdown-open:rotate-180
|
|
||||||
dropdown-toggle
|
|
||||||
duration-300
|
|
||||||
duration-500
|
|
||||||
ease-in-out
|
|
||||||
ease-linear
|
|
||||||
end-3
|
|
||||||
file-upload-complete:progress-success
|
|
||||||
fill-black
|
|
||||||
filter
|
|
||||||
filter-reset
|
|
||||||
fixed
|
|
||||||
flex
|
|
||||||
flex-1
|
|
||||||
flex-col
|
|
||||||
flex-grow
|
|
||||||
flex-nowrap
|
|
||||||
flex-row
|
|
||||||
flex-shrink-0
|
|
||||||
flex-wrap
|
|
||||||
focus-visible:outline-none
|
|
||||||
focus-within:border-primary
|
|
||||||
focus:border-primary
|
|
||||||
focus:outline-1
|
|
||||||
focus:outline-none
|
|
||||||
focus:outline-primary
|
|
||||||
focus:ring-0
|
|
||||||
font-bold
|
|
||||||
font-inter
|
|
||||||
font-medium
|
|
||||||
font-normal
|
|
||||||
font-semibold
|
|
||||||
gap-0.5
|
|
||||||
gap-1
|
|
||||||
gap-1.5
|
|
||||||
gap-2
|
|
||||||
gap-3
|
|
||||||
gap-4
|
|
||||||
gap-5
|
|
||||||
gap-6
|
|
||||||
gap-x-1
|
|
||||||
gap-x-2
|
|
||||||
gap-x-3
|
|
||||||
gap-y-1
|
|
||||||
gap-y-3
|
|
||||||
grid
|
|
||||||
grid-cols-1
|
|
||||||
grid-flow-col
|
|
||||||
grow
|
|
||||||
grow-0
|
|
||||||
h-12
|
|
||||||
h-2
|
|
||||||
h-32
|
|
||||||
h-64
|
|
||||||
h-8
|
|
||||||
h-auto
|
|
||||||
h-full
|
|
||||||
h-screen
|
|
||||||
helper-text
|
|
||||||
hidden
|
|
||||||
hover:bg-primary/5
|
|
||||||
hover:bg-transparent
|
|
||||||
hover:text-base-content
|
|
||||||
hover:text-base-content/60
|
|
||||||
hover:text-primary
|
|
||||||
image-full
|
|
||||||
inline
|
|
||||||
inline-block
|
|
||||||
inline-flex
|
|
||||||
inline-grid
|
|
||||||
input
|
|
||||||
input-floating
|
|
||||||
input-floating-label
|
|
||||||
input-lg
|
|
||||||
input-md
|
|
||||||
input-sm
|
|
||||||
input-xl
|
|
||||||
inset-0
|
|
||||||
inset-y-0
|
|
||||||
inset-y-2
|
|
||||||
invisible
|
|
||||||
is-invalid
|
|
||||||
is-valid
|
|
||||||
isolate
|
|
||||||
italic
|
|
||||||
items-center
|
|
||||||
items-end
|
|
||||||
items-start
|
|
||||||
join
|
|
||||||
join-item
|
|
||||||
justify-between
|
|
||||||
justify-center
|
|
||||||
justify-end
|
|
||||||
justify-start
|
|
||||||
justify-stretch
|
|
||||||
label-text
|
|
||||||
leading-3
|
|
||||||
leading-3.5
|
|
||||||
leading-6
|
|
||||||
left-0
|
|
||||||
lg:bg-base-100/20
|
|
||||||
lg:flex
|
|
||||||
lg:gap-y-0
|
|
||||||
lg:grid-cols-2
|
|
||||||
lg:hidden
|
|
||||||
lg:justify-end
|
|
||||||
lg:justify-start
|
|
||||||
lg:min-w-0
|
|
||||||
lg:p-10
|
|
||||||
lg:pb-5
|
|
||||||
lg:pl-64
|
|
||||||
lg:pr-3.5
|
|
||||||
lg:pt-5
|
|
||||||
lg:ring-1
|
|
||||||
lg:ring-base-content/10
|
|
||||||
lg:rounded-lg
|
|
||||||
lg:shadow-xs
|
|
||||||
list-disc
|
|
||||||
list-inside
|
|
||||||
loading
|
|
||||||
loading-lg
|
|
||||||
loading-sm
|
|
||||||
loading-spinner
|
|
||||||
loading-xl
|
|
||||||
loading-xs
|
|
||||||
lowercase
|
|
||||||
m-10
|
|
||||||
mask
|
|
||||||
max-h-52
|
|
||||||
max-lg:flex-col
|
|
||||||
max-lg:hidden
|
|
||||||
max-w-7xl
|
|
||||||
max-w-80
|
|
||||||
max-w-full
|
|
||||||
max-w-lg
|
|
||||||
max-w-sm
|
|
||||||
max-w-xl
|
|
||||||
mb-0.5
|
|
||||||
mb-1
|
|
||||||
mb-2
|
|
||||||
mb-3
|
|
||||||
mb-4
|
|
||||||
mb-5
|
|
||||||
md:table-cell
|
|
||||||
md:text-3xl
|
|
||||||
me-1
|
|
||||||
me-1.5
|
|
||||||
me-2
|
|
||||||
me-2.5
|
|
||||||
me-5
|
|
||||||
menu
|
|
||||||
menu-active
|
|
||||||
menu-disabled
|
|
||||||
menu-dropdown
|
|
||||||
menu-dropdown-show
|
|
||||||
menu-focus
|
|
||||||
menu-horizontal
|
|
||||||
menu-title
|
|
||||||
min-h-0
|
|
||||||
min-h-svh
|
|
||||||
min-w-0
|
|
||||||
min-w-28
|
|
||||||
min-w-48
|
|
||||||
min-w-60
|
|
||||||
min-w-[100px]
|
|
||||||
ml-3
|
|
||||||
ml-4
|
|
||||||
modal
|
|
||||||
modal-content
|
|
||||||
modal-dialog
|
|
||||||
modal-middle
|
|
||||||
modal-title
|
|
||||||
mr-4
|
|
||||||
ms-1
|
|
||||||
ms-2
|
|
||||||
ms-3
|
|
||||||
mt-1
|
|
||||||
mt-1.5
|
|
||||||
mt-10
|
|
||||||
mt-12
|
|
||||||
mt-2
|
|
||||||
mt-2.5
|
|
||||||
mt-3
|
|
||||||
mt-4
|
|
||||||
mt-5
|
|
||||||
mt-8
|
|
||||||
mx-1
|
|
||||||
mx-auto
|
|
||||||
my-3
|
|
||||||
my-auto
|
|
||||||
opacity-0
|
|
||||||
opacity-100
|
|
||||||
open
|
|
||||||
origin-top-left
|
|
||||||
outline
|
|
||||||
outline-0
|
|
||||||
overflow-hidden
|
|
||||||
overflow-x-auto
|
|
||||||
overflow-y-auto
|
|
||||||
overlay-open:duration-50
|
|
||||||
overlay-open:opacity-100
|
|
||||||
p-0.5
|
|
||||||
p-1
|
|
||||||
p-2
|
|
||||||
p-3
|
|
||||||
p-4
|
|
||||||
p-5
|
|
||||||
p-6
|
|
||||||
p-8
|
|
||||||
pin-input
|
|
||||||
pin-input-underline
|
|
||||||
placeholder-base-content/60
|
|
||||||
pointer-events-auto
|
|
||||||
pointer-events-none
|
|
||||||
progress
|
|
||||||
progress-bar
|
|
||||||
progress-indeterminate
|
|
||||||
progress-primary
|
|
||||||
pt-0
|
|
||||||
pt-0.5
|
|
||||||
pt-3
|
|
||||||
px-1.5
|
|
||||||
px-2
|
|
||||||
px-2.5
|
|
||||||
px-3
|
|
||||||
px-4
|
|
||||||
px-5
|
|
||||||
py-0.5
|
|
||||||
py-1.5
|
|
||||||
py-2
|
|
||||||
py-2.5
|
|
||||||
py-6
|
|
||||||
radio
|
|
||||||
range
|
|
||||||
relative
|
|
||||||
resize
|
|
||||||
ring-0
|
|
||||||
ring-1
|
|
||||||
ring-white/10
|
|
||||||
rounded-box
|
|
||||||
rounded-field
|
|
||||||
rounded-full
|
|
||||||
rounded-lg
|
|
||||||
rounded-md
|
|
||||||
rounded-t-lg
|
|
||||||
row-active
|
|
||||||
row-hover
|
|
||||||
rtl:!mr-0
|
|
||||||
select
|
|
||||||
select-disabled:opacity-40
|
|
||||||
select-disabled:pointer-events-none
|
|
||||||
select-floating
|
|
||||||
select-floating-label
|
|
||||||
selected
|
|
||||||
selected:select-active
|
|
||||||
shadow-base-300/20
|
|
||||||
shadow-lg
|
|
||||||
shadow-xs
|
|
||||||
shrink-0
|
|
||||||
size-10
|
|
||||||
size-4
|
|
||||||
size-5
|
|
||||||
size-8
|
|
||||||
skeleton
|
|
||||||
skeleton-animated
|
|
||||||
sm:auto-cols-max
|
|
||||||
sm:flex
|
|
||||||
sm:items-center
|
|
||||||
sm:items-end
|
|
||||||
sm:justify-between
|
|
||||||
sm:justify-end
|
|
||||||
sm:max-w-2xl
|
|
||||||
sm:max-w-3xl
|
|
||||||
sm:max-w-4xl
|
|
||||||
sm:max-w-5xl
|
|
||||||
sm:max-w-6xl
|
|
||||||
sm:max-w-7xl
|
|
||||||
sm:max-w-lg
|
|
||||||
sm:max-w-md
|
|
||||||
sm:max-w-xl
|
|
||||||
sm:mb-0
|
|
||||||
sm:mt-5
|
|
||||||
sm:mt-6
|
|
||||||
sm:p-6
|
|
||||||
sm:py-2
|
|
||||||
sm:text-sm/5
|
|
||||||
space-x-1
|
|
||||||
space-y-1
|
|
||||||
space-y-4
|
|
||||||
sr-only
|
|
||||||
static
|
|
||||||
status
|
|
||||||
status-error
|
|
||||||
sticky
|
|
||||||
switch
|
|
||||||
tab
|
|
||||||
tab-active
|
|
||||||
table
|
|
||||||
table-pin-cols
|
|
||||||
table-pin-rows
|
|
||||||
tabs
|
|
||||||
tabs-bordered
|
|
||||||
tabs-lg
|
|
||||||
tabs-lifted
|
|
||||||
tabs-md
|
|
||||||
tabs-sm
|
|
||||||
tabs-xl
|
|
||||||
tabs-xs
|
|
||||||
text-2xl
|
|
||||||
text-4xl
|
|
||||||
text-accent
|
|
||||||
text-base
|
|
||||||
text-base-content
|
|
||||||
text-base-content/40
|
|
||||||
text-base-content/50
|
|
||||||
text-base-content/60
|
|
||||||
text-base-content/70
|
|
||||||
text-base-content/80
|
|
||||||
text-base/6
|
|
||||||
text-center
|
|
||||||
text-error
|
|
||||||
text-error-content
|
|
||||||
text-gray-400
|
|
||||||
text-info
|
|
||||||
text-info-content
|
|
||||||
text-left
|
|
||||||
text-lg
|
|
||||||
text-primary
|
|
||||||
text-primary-content
|
|
||||||
text-sm
|
|
||||||
text-sm/5
|
|
||||||
text-success
|
|
||||||
text-success-content
|
|
||||||
text-warning
|
|
||||||
text-warning-content
|
|
||||||
text-xl
|
|
||||||
text-xs
|
|
||||||
text-xs/5
|
|
||||||
textarea
|
|
||||||
textarea-floating
|
|
||||||
textarea-floating-label
|
|
||||||
theme-controller
|
|
||||||
tooltip
|
|
||||||
tooltip-content
|
|
||||||
top-0
|
|
||||||
top-1/2
|
|
||||||
top-full
|
|
||||||
transform
|
|
||||||
transition
|
|
||||||
transition-all
|
|
||||||
transition-opacity
|
|
||||||
translate-x-0
|
|
||||||
truncate
|
|
||||||
underline
|
|
||||||
uppercase
|
|
||||||
validate
|
|
||||||
w-0
|
|
||||||
w-0.5
|
|
||||||
w-12
|
|
||||||
w-4
|
|
||||||
w-56
|
|
||||||
w-64
|
|
||||||
w-fit
|
|
||||||
w-full
|
|
||||||
whitespace-nowrap
|
|
||||||
z-10
|
|
||||||
z-40
|
|
||||||
z-50
|
|
||||||
@@ -44,6 +44,7 @@ animate-bounce
|
|||||||
animate-ping
|
animate-ping
|
||||||
aria-[current='page']:text-bg-soft-primary
|
aria-[current='page']:text-bg-soft-primary
|
||||||
avatar
|
avatar
|
||||||
|
avatar-placeholder
|
||||||
badge
|
badge
|
||||||
badge-error
|
badge-error
|
||||||
badge-info
|
badge-info
|
||||||
@@ -73,16 +74,23 @@ block
|
|||||||
blur
|
blur
|
||||||
border
|
border
|
||||||
border-0
|
border-0
|
||||||
|
border-1
|
||||||
border-2
|
border-2
|
||||||
border-b
|
border-b
|
||||||
|
border-b-2
|
||||||
|
border-b-base-content/20
|
||||||
border-base-content
|
border-base-content
|
||||||
border-base-content/20
|
border-base-content/20
|
||||||
border-base-content/25
|
border-base-content/25
|
||||||
border-base-content/40
|
border-base-content/40
|
||||||
border-base-content/5
|
border-base-content/5
|
||||||
border-dashed
|
border-dashed
|
||||||
|
border-error/30
|
||||||
|
border-info/30
|
||||||
|
border-success/30
|
||||||
border-t
|
border-t
|
||||||
border-transparent
|
border-transparent
|
||||||
|
border-warning/30
|
||||||
bottom-0
|
bottom-0
|
||||||
bottom-full
|
bottom-full
|
||||||
break-words
|
break-words
|
||||||
@@ -93,7 +101,6 @@ btn-circle
|
|||||||
btn-disabled
|
btn-disabled
|
||||||
btn-error
|
btn-error
|
||||||
btn-info
|
btn-info
|
||||||
btn-outline
|
|
||||||
btn-primary
|
btn-primary
|
||||||
btn-secondary
|
btn-secondary
|
||||||
btn-sm
|
btn-sm
|
||||||
@@ -200,7 +207,7 @@ grid-cols-4
|
|||||||
grid-flow-col
|
grid-flow-col
|
||||||
grow
|
grow
|
||||||
grow-0
|
grow-0
|
||||||
h-12
|
h-10
|
||||||
h-2
|
h-2
|
||||||
h-3
|
h-3
|
||||||
h-32
|
h-32
|
||||||
@@ -245,11 +252,9 @@ justify-between
|
|||||||
justify-center
|
justify-center
|
||||||
justify-end
|
justify-end
|
||||||
justify-start
|
justify-start
|
||||||
justify-stretch
|
|
||||||
label-text
|
label-text
|
||||||
leading-3
|
leading-3
|
||||||
leading-3.5
|
leading-3.5
|
||||||
leading-6
|
|
||||||
leading-none
|
leading-none
|
||||||
left-0
|
left-0
|
||||||
lg:bg-base-100/20
|
lg:bg-base-100/20
|
||||||
@@ -287,6 +292,9 @@ mask
|
|||||||
max-h-52
|
max-h-52
|
||||||
max-lg:flex-col
|
max-lg:flex-col
|
||||||
max-lg:hidden
|
max-lg:hidden
|
||||||
|
max-md:flex-wrap
|
||||||
|
max-md:justify-center
|
||||||
|
max-sm:hidden
|
||||||
max-w-7xl
|
max-w-7xl
|
||||||
max-w-80
|
max-w-80
|
||||||
max-w-full
|
max-w-full
|
||||||
@@ -323,6 +331,7 @@ min-w-28
|
|||||||
min-w-48
|
min-w-48
|
||||||
min-w-60
|
min-w-60
|
||||||
min-w-[100px]
|
min-w-[100px]
|
||||||
|
min-w-full
|
||||||
min-w-sm
|
min-w-sm
|
||||||
ml-3
|
ml-3
|
||||||
ml-4
|
ml-4
|
||||||
@@ -330,10 +339,10 @@ modal
|
|||||||
modal-content
|
modal-content
|
||||||
modal-dialog
|
modal-dialog
|
||||||
modal-middle
|
modal-middle
|
||||||
modal-title
|
|
||||||
mr-4
|
mr-4
|
||||||
ms-0.5
|
ms-0.5
|
||||||
ms-1
|
ms-1
|
||||||
|
ms-1.5
|
||||||
ms-2
|
ms-2
|
||||||
ms-3
|
ms-3
|
||||||
ms-auto
|
ms-auto
|
||||||
@@ -368,11 +377,13 @@ p-0.5
|
|||||||
p-1
|
p-1
|
||||||
p-1.5
|
p-1.5
|
||||||
p-2
|
p-2
|
||||||
|
p-2.5
|
||||||
p-3
|
p-3
|
||||||
p-4
|
p-4
|
||||||
p-5
|
p-5
|
||||||
p-6
|
p-6
|
||||||
p-8
|
p-8
|
||||||
|
pb-1
|
||||||
pin-input
|
pin-input
|
||||||
placeholder-base-content/60
|
placeholder-base-content/60
|
||||||
pointer-events-auto
|
pointer-events-auto
|
||||||
@@ -383,7 +394,9 @@ progress-indeterminate
|
|||||||
progress-primary
|
progress-primary
|
||||||
pt-0
|
pt-0
|
||||||
pt-0.5
|
pt-0.5
|
||||||
|
pt-2
|
||||||
pt-3
|
pt-3
|
||||||
|
px-0.5
|
||||||
px-1.5
|
px-1.5
|
||||||
px-2
|
px-2
|
||||||
px-2.5
|
px-2.5
|
||||||
@@ -418,6 +431,7 @@ select-disabled:opacity-40
|
|||||||
select-disabled:pointer-events-none
|
select-disabled:pointer-events-none
|
||||||
select-floating
|
select-floating
|
||||||
select-floating-label
|
select-floating-label
|
||||||
|
select-sm
|
||||||
selected
|
selected
|
||||||
selected:select-active
|
selected:select-active
|
||||||
shadow-base-300/20
|
shadow-base-300/20
|
||||||
@@ -426,6 +440,7 @@ shadow-md
|
|||||||
shadow-xs
|
shadow-xs
|
||||||
shrink-0
|
shrink-0
|
||||||
size-10
|
size-10
|
||||||
|
size-12
|
||||||
size-4
|
size-4
|
||||||
size-5
|
size-5
|
||||||
size-8
|
size-8
|
||||||
@@ -448,11 +463,11 @@ sm:max-w-md
|
|||||||
sm:max-w-xl
|
sm:max-w-xl
|
||||||
sm:mb-0
|
sm:mb-0
|
||||||
sm:mt-5
|
sm:mt-5
|
||||||
sm:mt-6
|
|
||||||
sm:p-6
|
sm:p-6
|
||||||
sm:py-2
|
sm:py-2
|
||||||
sm:text-sm/5
|
sm:text-sm/5
|
||||||
space-x-1
|
space-x-1
|
||||||
|
space-x-2.5
|
||||||
space-y-1
|
space-y-1
|
||||||
space-y-4
|
space-y-4
|
||||||
sr-only
|
sr-only
|
||||||
@@ -495,6 +510,7 @@ text-left
|
|||||||
text-lg
|
text-lg
|
||||||
text-primary
|
text-primary
|
||||||
text-primary-content
|
text-primary-content
|
||||||
|
text-right
|
||||||
text-sm
|
text-sm
|
||||||
text-sm/5
|
text-sm/5
|
||||||
text-success
|
text-success
|
||||||
@@ -525,6 +541,7 @@ validate
|
|||||||
w-0
|
w-0
|
||||||
w-0.5
|
w-0.5
|
||||||
w-12
|
w-12
|
||||||
|
w-13
|
||||||
w-4
|
w-4
|
||||||
w-56
|
w-56
|
||||||
w-64
|
w-64
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
@using MoonCore.Helpers
|
@using MoonCore.Helpers
|
||||||
@using MoonCore.Models
|
@using MoonCore.Models
|
||||||
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
|
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
|
||||||
@using MoonCore.Blazor.FlyonUi.DataTables
|
@using MoonCore.Blazor.FlyonUi.Grid
|
||||||
|
@using MoonCore.Blazor.FlyonUi.Grid.Columns
|
||||||
|
@using MoonCore.Blazor.FlyonUi.Grid.ToolbarItems
|
||||||
|
|
||||||
@inject HttpApiClient ApiClient
|
@inject HttpApiClient ApiClient
|
||||||
@inject AlertService AlertService
|
@inject AlertService AlertService
|
||||||
@@ -47,48 +49,72 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5 flex justify-end">
|
<DataGrid @ref="Grid"
|
||||||
<a href="/admin/api/create" class="btn btn-primary">
|
TGridItem="ApiKeyResponse"
|
||||||
Create
|
ItemsProvider="ItemsProvider"
|
||||||
</a>
|
EnableFiltering="true"
|
||||||
</div>
|
EnablePagination="true">
|
||||||
|
<PropertyColumn Field="x => x.Id" Sortable="true" />
|
||||||
|
<PropertyColumn Field="x => x.Description" />
|
||||||
|
<TemplateColumn Sortable="true" Title="Expires At">
|
||||||
|
<td>
|
||||||
|
@Formatter.FormatDate(context.ExpiresAt.UtcDateTime)
|
||||||
|
</td>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Sortable="true" Title="Created At">
|
||||||
|
<td>
|
||||||
|
@Formatter.FormatDate(context.CreatedAt.UtcDateTime)
|
||||||
|
</td>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn>
|
||||||
|
<td>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<a href="/admin/api/@(context.Id)" class="text-primary mr-2 sm:mr-3">
|
||||||
|
<i class="icon-pencil text-base"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
<DataTable @ref="Table" TItem="ApiKeyResponse">
|
<a href="#" @onclick="() => DeleteAsync(context)" @onclick:preventDefault
|
||||||
<Configuration>
|
class="text-error">
|
||||||
<Pagination TItem="ApiKeyResponse" ItemSource="LoadData" />
|
<i class="icon-trash text-base"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</TemplateColumn>
|
||||||
|
|
||||||
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.Id)" Name="Id"/>
|
<TemplateToolbarItem>
|
||||||
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.Description)" Name="Description"/>
|
<a href="/admin/api/create" class="btn btn-primary ms-1.5">
|
||||||
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.ExpiresAt)" Name="Expires at">
|
Create
|
||||||
<ColumnTemplate>
|
</a>
|
||||||
@(Formatter.FormatDate(context.ExpiresAt.UtcDateTime))
|
</TemplateToolbarItem>
|
||||||
</ColumnTemplate>
|
</DataGrid>
|
||||||
</DataTableColumn>
|
|
||||||
<DataTableColumn TItem="ApiKeyResponse">
|
|
||||||
<ColumnTemplate>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<a href="/admin/api/@(context.Id)" class="text-primary mr-2 sm:mr-3">
|
|
||||||
<i class="icon-pencil text-base"></i>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault
|
|
||||||
class="text-error">
|
|
||||||
<i class="icon-trash text-base"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</ColumnTemplate>
|
|
||||||
</DataTableColumn>
|
|
||||||
</Configuration>
|
|
||||||
</DataTable>
|
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private DataTable<ApiKeyResponse> Table;
|
private DataGrid<ApiKeyResponse> Grid;
|
||||||
|
|
||||||
private async Task<IPagedData<ApiKeyResponse>> LoadData(PaginationOptions options)
|
private async Task<DataGridItemResult<ApiKeyResponse>> ItemsProvider(DataGridItemRequest request)
|
||||||
=> await ApiClient.GetJson<PagedData<ApiKeyResponse>>($"api/admin/apikeys?page={options.Page}&pageSize={options.PerPage}");
|
{
|
||||||
|
var query = $"?startIndex={request.StartIndex}&count={request.Count}";
|
||||||
|
|
||||||
private async Task Delete(ApiKeyResponse apiKeyResponse)
|
if (!string.IsNullOrEmpty(request.SortColumn))
|
||||||
|
{
|
||||||
|
var dir = request.SortDirection == SortState.Descending ? "desc" : "asc";
|
||||||
|
query += $"&orderBy={request.SortColumn}&orderByDir={dir}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.Filter))
|
||||||
|
query += $"&filter={request.Filter}";
|
||||||
|
|
||||||
|
var data = await ApiClient.GetJson<CountedData<ApiKeyResponse>>($"api/admin/apikeys{query}");
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Items = data.Items,
|
||||||
|
TotalCount = data.TotalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteAsync(ApiKeyResponse apiKeyResponse)
|
||||||
{
|
{
|
||||||
await AlertService.ConfirmDanger(
|
await AlertService.ConfirmDanger(
|
||||||
"API Key deletion",
|
"API Key deletion",
|
||||||
@@ -98,7 +124,7 @@
|
|||||||
await ApiClient.Delete($"api/admin/apikeys/{apiKeyResponse.Id}");
|
await ApiClient.Delete($"api/admin/apikeys/{apiKeyResponse.Id}");
|
||||||
await ToastService.Success("Successfully deleted api key");
|
await ToastService.Success("Successfully deleted api key");
|
||||||
|
|
||||||
await Table.Refresh();
|
await Grid.RefreshAsync();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using MoonCore.Blazor.FlyonUi.DataTables
|
@using MoonCore.Blazor.FlyonUi.Grid
|
||||||
|
@using MoonCore.Blazor.FlyonUi.Grid.Columns
|
||||||
|
@using MoonCore.Blazor.FlyonUi.Grid.ToolbarItems
|
||||||
@using MoonCore.Blazor.FlyonUi.Helpers
|
@using MoonCore.Blazor.FlyonUi.Helpers
|
||||||
@using MoonCore.Helpers
|
@using MoonCore.Helpers
|
||||||
@using MoonCore.Models
|
@using MoonCore.Models
|
||||||
@@ -16,6 +18,7 @@
|
|||||||
@inject ThemeService ThemeService
|
@inject ThemeService ThemeService
|
||||||
@inject AlertService AlertService
|
@inject AlertService AlertService
|
||||||
@inject ToastService ToastService
|
@inject ToastService ToastService
|
||||||
|
@inject HttpApiClient ApiClient
|
||||||
@inject DownloadService DownloadService
|
@inject DownloadService DownloadService
|
||||||
@inject ILogger<Index> Logger
|
@inject ILogger<Index> Logger
|
||||||
|
|
||||||
@@ -25,72 +28,72 @@
|
|||||||
Themes
|
Themes
|
||||||
</PageSeparator>
|
</PageSeparator>
|
||||||
|
|
||||||
<div class="mt-5 flex justify-end">
|
<div class="my-8">
|
||||||
<div>
|
<DataGrid TGridItem="ThemeResponse"
|
||||||
<label for="import-theme" class="btn btn-info me-1">
|
ItemsProvider="ItemsProvider"
|
||||||
<i class="icon-file-up"></i>
|
EnableFiltering="true"
|
||||||
Import
|
EnablePagination="true">
|
||||||
</label>
|
|
||||||
<InputFile OnChange="Import" id="import-theme" class="hidden" multiple />
|
|
||||||
<a href="/admin/system/customisation/themes/create" class="btn btn-primary">Create</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-2.5">
|
<PropertyColumn Field="x => x.Id" Sortable="true" />
|
||||||
<DataTable @ref="Table" TItem="ThemeResponse">
|
<TemplateColumn Title="Name" Sortable="true">
|
||||||
<Configuration>
|
<td>
|
||||||
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Id)" Name="Id"/>
|
<div class="flex items-center">
|
||||||
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Name)" Name="Name">
|
@context.Name
|
||||||
<ColumnTemplate>
|
|
||||||
<div class="flex items-center">
|
|
||||||
@context.Name
|
|
||||||
|
|
||||||
@if (context.IsEnabled)
|
@if (context.IsEnabled)
|
||||||
{
|
{
|
||||||
<i class="icon-check text-success ms-2"></i>
|
<i class="icon-check text-success ms-2"></i>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</ColumnTemplate>
|
</td>
|
||||||
</DataTableColumn>
|
</TemplateColumn>
|
||||||
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Author)" Name="Author"/>
|
<PropertyColumn Field="x => x.Version" Sortable="true" />
|
||||||
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Version)" Name="Version"/>
|
<PropertyColumn Field="x => x.Author" />
|
||||||
<DataTableColumn TItem="ThemeResponse">
|
|
||||||
<ColumnTemplate>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
@if (!string.IsNullOrEmpty(context.DonateUrl))
|
|
||||||
{
|
|
||||||
<a href="@context.DonateUrl" target="_blank" class="flex items-center mr-2 sm:mr-3">
|
|
||||||
<i class="text-accent icon-heart me-1"></i>
|
|
||||||
<span class="text-accent">Donate</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(context.UpdateUrl))
|
<TemplateColumn>
|
||||||
{
|
<td>
|
||||||
<a href="#" class="flex items-center mr-2 sm:mr-3">
|
<div class="flex justify-end">
|
||||||
<i class="text-info icon-refresh-cw me-1"></i>
|
@if (!string.IsNullOrEmpty(context.DonateUrl))
|
||||||
<span class="text-info">Update</span>
|
{
|
||||||
</a>
|
<a href="@context.DonateUrl" target="_blank" class="flex items-center mr-2 sm:mr-3">
|
||||||
}
|
<i class="text-accent icon-heart me-1"></i>
|
||||||
|
<span class="text-accent">Donate</span>
|
||||||
<a @onclick="() => Export(context)" @onclick:preventDefault href="#" class="flex items-center mr-2 sm:mr-3">
|
|
||||||
<i class="text-success icon-download me-1"></i>
|
|
||||||
<span class="text-success">Export</span>
|
|
||||||
</a>
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
<a href="/admin/system/customisation/themes/@(context.Id)" class="mr-2 sm:mr-3">
|
@if (!string.IsNullOrEmpty(context.UpdateUrl))
|
||||||
<i class="icon-pencil text-primary"></i>
|
{
|
||||||
|
<a href="#" class="flex items-center mr-2 sm:mr-3">
|
||||||
|
<i class="text-info icon-refresh-cw me-1"></i>
|
||||||
|
<span class="text-info">Update</span>
|
||||||
</a>
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault>
|
<a @onclick="() => ExportAsync(context)" @onclick:preventDefault href="#" class="flex items-center mr-2 sm:mr-3">
|
||||||
<i class="icon-trash text-error"></i>
|
<i class="text-success icon-download me-1"></i>
|
||||||
</a>
|
<span class="text-success">Export</span>
|
||||||
</div>
|
</a>
|
||||||
</ColumnTemplate>
|
|
||||||
</DataTableColumn>
|
<a href="/admin/system/customisation/themes/@(context.Id)" class="mr-2 sm:mr-3">
|
||||||
<Pagination TItem="ThemeResponse" PageSize="10" ItemSource="LoadItems"/>
|
<i class="icon-pencil text-primary"></i>
|
||||||
</Configuration>
|
</a>
|
||||||
</DataTable>
|
|
||||||
|
<a href="#" @onclick="() => DeleteAsync(context)" @onclick:preventDefault>
|
||||||
|
<i class="icon-trash text-error"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</TemplateColumn>
|
||||||
|
|
||||||
|
<TemplateToolbarItem>
|
||||||
|
<label for="import-theme" class="btn btn-info me-1 ms-1.5">
|
||||||
|
<i class="icon-file-up"></i>
|
||||||
|
Import
|
||||||
|
</label>
|
||||||
|
<InputFile OnChange="ImportAsync" id="import-theme" class="hidden" multiple />
|
||||||
|
<a href="/admin/system/customisation/themes/create" class="btn btn-primary">Create</a>
|
||||||
|
</TemplateToolbarItem>
|
||||||
|
</DataGrid>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PageSeparator Icon="icon-images">
|
<PageSeparator Icon="icon-images">
|
||||||
@@ -100,12 +103,31 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private DataTable<ThemeResponse> Table;
|
private DataGrid<ThemeResponse> Grid;
|
||||||
|
|
||||||
private async Task<IPagedData<ThemeResponse>> LoadItems(PaginationOptions options)
|
private async Task<DataGridItemResult<ThemeResponse>> ItemsProvider(DataGridItemRequest request)
|
||||||
=> await ThemeService.Get(options.Page, options.PerPage);
|
{
|
||||||
|
var query = $"?startIndex={request.StartIndex}&count={request.Count}";
|
||||||
|
|
||||||
private async Task Import(InputFileChangeEventArgs eventArgs)
|
if (!string.IsNullOrEmpty(request.SortColumn))
|
||||||
|
{
|
||||||
|
var dir = request.SortDirection == SortState.Descending ? "desc" : "asc";
|
||||||
|
query += $"&orderBy={request.SortColumn}&orderByDir={dir}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.Filter))
|
||||||
|
query += $"&filter={request.Filter}";
|
||||||
|
|
||||||
|
var data = await ApiClient.GetJson<CountedData<ThemeResponse>>($"api/admin/system/customisation/themes{query}");
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Items = data.Items,
|
||||||
|
TotalCount = data.TotalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ImportAsync(InputFileChangeEventArgs eventArgs)
|
||||||
{
|
{
|
||||||
if(eventArgs.FileCount < 1)
|
if(eventArgs.FileCount < 1)
|
||||||
return;
|
return;
|
||||||
@@ -141,7 +163,7 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var theme = await ThemeService.Create(new CreateThemeRequest()
|
var theme = await ThemeService.CreateAsync(new CreateThemeRequest()
|
||||||
{
|
{
|
||||||
Name = themeTransfer.Name,
|
Name = themeTransfer.Name,
|
||||||
Author = themeTransfer.Author,
|
Author = themeTransfer.Author,
|
||||||
@@ -153,7 +175,7 @@
|
|||||||
|
|
||||||
await ToastService.Success("Successfully imported theme", theme.Name);
|
await ToastService.Success("Successfully imported theme", theme.Name);
|
||||||
|
|
||||||
await Table.Refresh();
|
await Grid.RefreshAsync();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -162,7 +184,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Export(ThemeResponse theme)
|
private async Task ExportAsync(ThemeResponse theme)
|
||||||
{
|
{
|
||||||
var transfer = new ThemeTransferModel()
|
var transfer = new ThemeTransferModel()
|
||||||
{
|
{
|
||||||
@@ -184,17 +206,17 @@
|
|||||||
await DownloadService.Download(fileName, json);
|
await DownloadService.Download(fileName, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Delete(ThemeResponse response)
|
private async Task DeleteAsync(ThemeResponse response)
|
||||||
{
|
{
|
||||||
await AlertService.ConfirmDanger(
|
await AlertService.ConfirmDanger(
|
||||||
"Theme deletion",
|
"Theme deletion",
|
||||||
$"Do you really want to delete the theme: {response.Name}",
|
$"Do you really want to delete the theme: {response.Name}",
|
||||||
async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
await ThemeService.Delete(response.Id);
|
await ThemeService.DeleteAsync(response.Id);
|
||||||
|
|
||||||
await ToastService.Success("Successfully deleted theme");
|
await ToastService.Success("Successfully deleted theme");
|
||||||
await Table.Refresh();
|
await Grid.RefreshAsync();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
private async Task OnValidSubmit()
|
private async Task OnValidSubmit()
|
||||||
{
|
{
|
||||||
await ThemeService.Create(Request);
|
await ThemeService.CreateAsync(Request);
|
||||||
await ToastService.Success("Successfully created theme");
|
await ToastService.Success("Successfully created theme");
|
||||||
|
|
||||||
NavigationManager.NavigateTo("/admin/system/customisation");
|
NavigationManager.NavigateTo("/admin/system/customisation");
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
private async Task Load(LazyLoader _)
|
private async Task Load(LazyLoader _)
|
||||||
{
|
{
|
||||||
Response = await ThemeService.Get(Id);
|
Response = await ThemeService.GetAsync(Id);
|
||||||
|
|
||||||
Request = new()
|
Request = new()
|
||||||
{
|
{
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
|
|
||||||
private async Task OnValidSubmit()
|
private async Task OnValidSubmit()
|
||||||
{
|
{
|
||||||
await ThemeService.Update(Id, Request);
|
await ThemeService.UpdateAsync(Id, Request);
|
||||||
|
|
||||||
await ToastService.Success("Successfully updated theme");
|
await ToastService.Success("Successfully updated theme");
|
||||||
Navigation.NavigateTo("/admin/system/customisation");
|
Navigation.NavigateTo("/admin/system/customisation");
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
.ToDictionary(x => x, _ => true);
|
.ToDictionary(x => x, _ => true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GenerateDiagnose(WButton _)
|
private async Task GenerateDiagnose(WButton button)
|
||||||
{
|
{
|
||||||
var request = new GenerateDiagnoseRequest();
|
var request = new GenerateDiagnoseRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
@using MoonCore.Helpers
|
@using MoonCore.Helpers
|
||||||
@using MoonCore.Models
|
@using MoonCore.Models
|
||||||
@using Moonlight.Shared.Http.Responses.Admin.Users
|
@using Moonlight.Shared.Http.Responses.Admin.Users
|
||||||
@using MoonCore.Blazor.FlyonUi.DataTables
|
@using MoonCore.Blazor.FlyonUi.Grid
|
||||||
|
@using MoonCore.Blazor.FlyonUi.Grid.Columns
|
||||||
|
|
||||||
@inject HttpApiClient ApiClient
|
@inject HttpApiClient ApiClient
|
||||||
@inject AlertService AlertService
|
@inject AlertService AlertService
|
||||||
@inject ToastService ToastService
|
@inject ToastService ToastService
|
||||||
|
|
||||||
<div class="mb-5">
|
<div class="mb-8">
|
||||||
<PageHeader Title="Users">
|
<PageHeader Title="Users">
|
||||||
<a href="/admin/users/create" class="btn btn-primary">
|
<a href="/admin/users/create" class="btn btn-primary">
|
||||||
Create
|
Create
|
||||||
@@ -17,37 +18,57 @@
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable @ref="Table" TItem="UserResponse">
|
<DataGrid @ref="Grid"
|
||||||
<Configuration>
|
TGridItem="UserResponse"
|
||||||
<Pagination TItem="UserResponse" ItemSource="LoadData" />
|
ItemsProvider="ItemsProvider"
|
||||||
|
EnableFiltering="true"
|
||||||
|
EnablePagination="true">
|
||||||
|
<PropertyColumn Field="x => x.Id" Sortable="true" />
|
||||||
|
<PropertyColumn Field="x => x.Username" Sortable="true" />
|
||||||
|
<PropertyColumn Field="x => x.Email" Sortable="true" />
|
||||||
|
|
||||||
<DataTableColumn TItem="UserResponse" Field="@(x => x.Id)" Name="Id"/>
|
<TemplateColumn>
|
||||||
<DataTableColumn TItem="UserResponse" Field="@(x => x.Username)" Name="Username"/>
|
<td>
|
||||||
<DataTableColumn TItem="UserResponse" Field="@(x => x.Email)" Name="Email"/>
|
<div class="flex justify-end">
|
||||||
<DataTableColumn TItem="UserResponse">
|
<a href="/admin/users/@(context.Id)" class="mr-2 sm:mr-3">
|
||||||
<ColumnTemplate>
|
<i class="icon-pencil text-primary"></i>
|
||||||
<div class="flex justify-end">
|
</a>
|
||||||
<a href="/admin/users/@(context.Id)" class="mr-2 sm:mr-3">
|
|
||||||
<i class="icon-pencil text-primary"></i>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault>
|
<a href="#" @onclick="() => DeleteAsync(context)" @onclick:preventDefault>
|
||||||
<i class="icon-trash text-error"></i>
|
<i class="icon-trash text-error"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</ColumnTemplate>
|
</td>
|
||||||
</DataTableColumn>
|
</TemplateColumn>
|
||||||
</Configuration>
|
</DataGrid>
|
||||||
</DataTable>
|
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private DataTable<UserResponse> Table;
|
private DataGrid<UserResponse> Grid;
|
||||||
|
|
||||||
private async Task<IPagedData<UserResponse>> LoadData(PaginationOptions options)
|
private async Task<DataGridItemResult<UserResponse>> ItemsProvider(DataGridItemRequest request)
|
||||||
=> await ApiClient.GetJson<PagedData<UserResponse>>($"api/admin/users?page={options.Page}&pageSize={options.PerPage}");
|
{
|
||||||
|
var query = $"?startIndex={request.StartIndex}&count={request.Count}";
|
||||||
|
|
||||||
private async Task Delete(UserResponse response)
|
if (!string.IsNullOrEmpty(request.SortColumn))
|
||||||
|
{
|
||||||
|
var dir = request.SortDirection == SortState.Descending ? "desc" : "asc";
|
||||||
|
query += $"&orderBy={request.SortColumn}&orderByDir={dir}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.Filter))
|
||||||
|
query += $"&filter={request.Filter}";
|
||||||
|
|
||||||
|
var data = await ApiClient.GetJson<CountedData<UserResponse>>($"api/admin/users{query}");
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Items = data.Items,
|
||||||
|
TotalCount = data.TotalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteAsync(UserResponse response)
|
||||||
{
|
{
|
||||||
await AlertService.ConfirmDanger(
|
await AlertService.ConfirmDanger(
|
||||||
"User deletion",
|
"User deletion",
|
||||||
@@ -57,7 +78,7 @@
|
|||||||
await ApiClient.Delete($"api/admin/users/{response.Id}");
|
await ApiClient.Delete($"api/admin/users/{response.Id}");
|
||||||
await ToastService.Success("Successfully deleted user");
|
await ToastService.Success("Successfully deleted user");
|
||||||
|
|
||||||
await Table.Refresh();
|
await Grid.RefreshAsync();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ public class ApiKeyResponse
|
|||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
public string[] Permissions { get; set; } = [];
|
public string[] Permissions { get; set; } = [];
|
||||||
public DateTimeOffset ExpiresAt { get; set; }
|
public DateTimeOffset ExpiresAt { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user