Skip to content

Commit

Permalink
feat(admin): config "autoDestroyOnLimitReached"
Browse files Browse the repository at this point in the history
  • Loading branch information
GZTimeWalker committed Jun 30, 2023
1 parent 839a09b commit dde5357
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 9 deletions.
8 changes: 8 additions & 0 deletions src/GZCTF/ClientApp/src/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ export interface ConfigEditModel {
accountPolicy?: AccountPolicy | null
/** 全局配置项 */
globalConfig?: GlobalConfig | null
/** 比赛策略 */
gamePolicy?: GamePolicy | null
}

/** 账户策略 */
Expand All @@ -252,6 +254,12 @@ export interface GlobalConfig {
slogan?: string
}

/** 比赛策略 */
export interface GamePolicy {
/** 是否在达到数量限制时自动销毁最早的容器 */
autoDestroyOnLimitReached?: boolean
}

/** 列表响应 */
export interface ArrayResponseOfUserInfoModel {
/** 数据 */
Expand Down
28 changes: 26 additions & 2 deletions src/GZCTF/ClientApp/src/pages/admin/Configs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SwitchLabel } from '@Components/admin/SwitchLabel'
import { showErrorNotification } from '@Utils/ApiErrorHandler'
import { useFixedButtonStyles } from '@Utils/ThemeOverride'
import { useConfig } from '@Utils/useConfig'
import api, { AccountPolicy, ConfigEditModel, GlobalConfig } from '@Api'
import api, { AccountPolicy, ConfigEditModel, GamePolicy, GlobalConfig } from '@Api'

const Configs: FC = () => {
const { data: configs, mutate } = api.admin.useAdminGetConfigs({
Expand All @@ -19,6 +19,8 @@ const Configs: FC = () => {
const [disabled, setDisabled] = useState(false)
const [globalConfig, setGlobalConfig] = useState<GlobalConfig | null>()
const [accountPolicy, setAccountPolicy] = useState<AccountPolicy | null>()
const [gamePolicy, setGamePolicy] = useState<GamePolicy | null>()

const [saved, setSaved] = useState(true)
const { classes: btnClasses } = useFixedButtonStyles({
right: 'calc(0.05 * (100vw - 70px - 2rem) + 1rem)',
Expand All @@ -27,6 +29,7 @@ const Configs: FC = () => {

useEffect(() => {
if (configs) {
setGamePolicy(configs.gamePolicy)
setGlobalConfig(configs.globalConfig)
setAccountPolicy(configs.accountPolicy)
}
Expand Down Expand Up @@ -55,7 +58,7 @@ const Configs: FC = () => {
size="md"
leftIcon={<Icon path={saved ? mdiContentSaveOutline : mdiCheck} size={1} />}
onClick={() => {
updateConfig({ globalConfig, accountPolicy })
updateConfig({ globalConfig, accountPolicy, gamePolicy })
setSaved(false)
setTimeout(() => setSaved(true), 500)
}}
Expand Down Expand Up @@ -148,6 +151,27 @@ const Configs: FC = () => {
}}
/>
</Stack>

<Stack>
<Title order={2}>比赛策略</Title>
<Divider />
<SimpleGrid cols={2}>
<Switch
checked={gamePolicy?.autoDestroyOnLimitReached ?? true}
disabled={disabled}
label={SwitchLabel(
'自动销毁旧实例',
'是否在用户开启题目实例但达到上限时自动销毁旧实例'
)}
onChange={(e) =>
setGamePolicy({
...(gamePolicy ?? {}),
autoDestroyOnLimitReached: e.currentTarget.checked,
})
}
/>
</SimpleGrid>
</Stack>
</Stack>
</AdminPage>
)
Expand Down
6 changes: 3 additions & 3 deletions src/GZCTF/ClientApp/src/pages/admin/games/[id]/Info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,10 @@ const GameInfoEdit: FC = () => {
onChange={(e) => game && setGame({ ...game, teamMemberCountLimit: Number(e) })}
/>
<NumberInput
label="队伍容器数量限制"
description="整个队伍共享的容器数量限制"
label="队伍容器数量上限"
description="队伍共享的容器数量 (0 表示不限制)"
disabled={disabled}
min={1}
min={0}
required
value={game?.containerCountLimit}
onChange={(e) => game && setGame({ ...game, containerCountLimit: Number(e) })}
Expand Down
3 changes: 2 additions & 1 deletion src/GZCTF/Controllers/AdminController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ public IActionResult GetConfigs()
ConfigEditModel config = new()
{
AccountPolicy = serviceProvider.GetRequiredService<IOptionsSnapshot<AccountPolicy>>().Value,
GlobalConfig = serviceProvider.GetRequiredService<IOptionsSnapshot<GlobalConfig>>().Value
GlobalConfig = serviceProvider.GetRequiredService<IOptionsSnapshot<GlobalConfig>>().Value,
GamePolicy = serviceProvider.GetRequiredService<IOptionsSnapshot<GamePolicy>>().Value
};

return Ok(config);
Expand Down
11 changes: 11 additions & 0 deletions src/GZCTF/Models/Internal/Configs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ public class AccountPolicy
public string EmailDomainList { get; set; } = string.Empty;
}

/// <summary>
/// 比赛策略
/// </summary>
public class GamePolicy
{
/// <summary>
/// 是否在达到数量限制时自动销毁最早的容器
/// </summary>
public bool AutoDestroyOnLimitReached { get; set; } = false;
}

/// <summary>
/// 全局设置
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/GZCTF/Models/Request/Admin/ConfigEditModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ public class ConfigEditModel
/// 全局配置项
/// </summary>
public GlobalConfig? GlobalConfig { get; set; }

/// <summary>
/// 比赛策略
/// </summary>
public GamePolicy? GamePolicy { get; set; }
}
1 change: 1 addition & 0 deletions src/GZCTF/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
builder.Services.Configure<RegistryConfig>(builder.Configuration.GetSection(nameof(RegistryConfig)));
builder.Services.Configure<AccountPolicy>(builder.Configuration.GetSection(nameof(AccountPolicy)));
builder.Services.Configure<GlobalConfig>(builder.Configuration.GetSection(nameof(GlobalConfig)));
builder.Services.Configure<GamePolicy>(builder.Configuration.GetSection(nameof(GamePolicy)));
builder.Services.Configure<ContainerProvider>(builder.Configuration.GetSection(nameof(ContainerProvider)));

if (builder.Configuration.GetSection(nameof(ContainerProvider))
Expand Down
1 change: 1 addition & 0 deletions src/GZCTF/Providers/EntityConfigurationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ private static HashSet<Config> DefaultConfigs()

configs.UnionWith(ConfigService.GetConfigs(new AccountPolicy()));
configs.UnionWith(ConfigService.GetConfigs(new GlobalConfig()));
configs.UnionWith(ConfigService.GetConfigs(new GamePolicy()));

return configs;
}
Expand Down
33 changes: 30 additions & 3 deletions src/GZCTF/Repositories/InstanceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using CTFServer.Services.Interface;
using CTFServer.Utils;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace CTFServer.Repositories;

Expand All @@ -12,15 +13,18 @@ public class InstanceRepository : RepositoryBase, IInstanceRepository
private readonly IContainerRepository containerRepository;
private readonly IGameEventRepository gameEventRepository;
private readonly ILogger<InstanceRepository> logger;
private readonly IOptionsSnapshot<GamePolicy> gamePolicy;

public InstanceRepository(AppDbContext _context,
IContainerService _service,
IContainerRepository _containerRepository,
IGameEventRepository _gameEventRepository,
IOptionsSnapshot<GamePolicy> _gamePolicy,
ILogger<InstanceRepository> _logger) : base(_context)
{
logger = _logger;
service = _service;
gamePolicy = _gamePolicy;
gameEventRepository = _gameEventRepository;
containerRepository = _containerRepository;
}
Expand Down Expand Up @@ -128,9 +132,32 @@ public async Task<TaskResult<Container>> CreateContainer(Instance instance, Team
return new TaskResult<Container>(TaskStatus.Failed);
}

if (await context.Instances.CountAsync(i => i.Participation == instance.Participation
&& i.Container != null, token) >= containerLimit)
return new TaskResult<Container>(TaskStatus.Denied);
// containerLimit == 0 means unlimit
if (containerLimit > 0)
{
if (gamePolicy.Value.AutoDestroyOnLimitReached)
{
var running = await context.Instances
.Where(i => i.Participation == instance.Participation && i.Container != null)
.OrderBy(i => i.Container!.StartedAt).ToListAsync(token);

var first = running.FirstOrDefault();
if (running.Count >= containerLimit && first is not null)
{
logger.Log($"{team.Name} 自动销毁题目 {first.Challenge.Title} 的容器实例 [{first.Container!.ContainerId}]", user, TaskStatus.Success);
await DestroyContainer(running.First().Container!, token);
}
}
else
{
var count = await context.Instances.CountAsync(
i => i.Participation == instance.Participation &&
i.Container != null, token);

if (count >= containerLimit)
return new TaskResult<Container>(TaskStatus.Denied);
}
}

if (instance.Container is null)
{
Expand Down

0 comments on commit dde5357

Please sign in to comment.