Enhance activation request validation and documentation
- Updated the `RequestActivationCommandValidator` to require a non-empty comment, ensuring that users provide necessary identification information. - Modified integration tests to validate the new requirement for a comment, including a test for handling empty comments. - Updated API documentation to reflect that the comment is now mandatory and clarified its purpose. - Enhanced frontend components to enforce comment requirements and provide user guidance on the comment's importance.
This commit is contained in:
@@ -6,6 +6,7 @@ public sealed class RequestActivationCommandValidator : AbstractValidator<Reques
|
||||
{
|
||||
public RequestActivationCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Comment).MaximumLength(500);
|
||||
// Комментарий обязателен: по нему админ понимает, кто заявитель и откуда.
|
||||
RuleFor(x => x.Comment).NotEmpty().MaximumLength(500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public class ActivationFlowTests(PnvPanelWebApplicationFactory factory)
|
||||
|
||||
var requestResponse = await userClient.PostJsonAsync(
|
||||
"/api/activation/request",
|
||||
new { comment = (string?)null }
|
||||
new { comment = "Erin, a friend of the admin" }
|
||||
);
|
||||
var request = await requestResponse.ReadAsAsync<ActivationRequestResponse>();
|
||||
|
||||
@@ -85,14 +85,29 @@ public class ActivationFlowTests(PnvPanelWebApplicationFactory factory)
|
||||
|
||||
var first = await userClient.PostJsonAsync(
|
||||
"/api/activation/request",
|
||||
new { comment = (string?)null }
|
||||
new { comment = "Frank, a colleague of the admin" }
|
||||
);
|
||||
Assert.Equal(HttpStatusCode.OK, first.StatusCode);
|
||||
|
||||
var second = await userClient.PostJsonAsync(
|
||||
"/api/activation/request",
|
||||
new { comment = (string?)null }
|
||||
new { comment = "Frank, a colleague of the admin" }
|
||||
);
|
||||
Assert.Equal(HttpStatusCode.Conflict, second.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Request_WhenCommentEmpty_ReturnsBadRequest()
|
||||
{
|
||||
using var userClient = factory.CreateClient();
|
||||
var userName = $"gina_{Guid.NewGuid():N}"[..20];
|
||||
var (_, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123");
|
||||
userClient.UseBearerToken(userToken);
|
||||
|
||||
var response = await userClient.PostJsonAsync(
|
||||
"/api/activation/request",
|
||||
new { comment = (string?)null }
|
||||
);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ status, createdAt }`. `expiresAt` всегда `null` (лимиты по сро
|
||||
| Метод | Путь | Роль | Тело запроса | Тело ответа |
|
||||
| ----- | --------------------------- | ---- | ----------------- | -------------------------------------------------------- |
|
||||
| GET | `/api/activation/status` | user | — | `{ isActivated, pendingRequest: { id, comment, createdAt } \| null }` |
|
||||
| POST | `/api/activation/request` | user | `{ comment? }` | `{ id, comment, createdAt }` |
|
||||
| POST | `/api/activation/request` | user | `{ comment }` (обязателен, ≤500) | `{ id, comment, createdAt }` |
|
||||
|
||||
## Billing (пользователь)
|
||||
|
||||
|
||||
@@ -706,7 +706,7 @@ Application-хендлере поверх результата `IIdentityService
|
||||
| ------------ | ----------------------- | ---------------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `UserId` | `Guid` | FK → AppUser (заявитель) |
|
||||
| `Comment` | `string?` | Комментарий заявителя, напр. «я Никита» — чтобы админ понял, кто это |
|
||||
| `Comment` | `string?` | Комментарий заявителя (кто и откуда), напр. «я Никита, коллега Артёма» — обязателен при создании заявки через API (`NotEmpty`, ≤500); nullable в схеме ради исторических записей |
|
||||
| `Status` | `ActivationStatus` | `Pending` / `Approved` / `Rejected` |
|
||||
| `DecidedBy` | `Guid?` | Админ, принявший решение |
|
||||
| `DecidedAt` | `DateTimeOffset?` | |
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@ PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует
|
||||
|
||||
### Активация пользователей
|
||||
- После регистрации пользователь **не активирован** и не может создавать конфиги.
|
||||
- Он отправляет **запрос на активацию** с комментарием (напр. «я Никита» — чтобы админ понял, кто это).
|
||||
- Он отправляет **запрос на активацию** с обязательным комментарием — кто он и откуда (напр. «я Никита,
|
||||
коллега Артёма»), чтобы админ понял, кто это.
|
||||
- Админ одобряет/отклоняет запрос **на сайте или в Telegram**. После одобрения — доступно создание конфигов.
|
||||
|
||||
### Аутентификация и восстановление доступа
|
||||
|
||||
@@ -37,10 +37,13 @@ export function ActivationGate({ children }: { children: ReactNode }) {
|
||||
}
|
||||
if (data.isActivated) return <>{children}</>
|
||||
|
||||
const trimmedComment = comment.trim()
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!trimmedComment) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await requestActivation(comment.trim() || undefined)
|
||||
await requestActivation(trimmedComment)
|
||||
await queryClient.invalidateQueries({ queryKey: ['activation-status'] })
|
||||
} catch (error) {
|
||||
const message = error instanceof HttpError && error.status === 409 ? t('activation.alreadyPending') : t('auth.genericError')
|
||||
@@ -66,12 +69,17 @@ export function ActivationGate({ children }: { children: ReactNode }) {
|
||||
<Label htmlFor="comment">{t('activation.commentLabel')}</Label>
|
||||
<textarea
|
||||
id="comment"
|
||||
className="min-h-24 rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
required
|
||||
aria-required="true"
|
||||
maxLength={500}
|
||||
placeholder={t('activation.commentPlaceholder')}
|
||||
className="min-h-24 rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('activation.commentHint')}</p>
|
||||
</div>
|
||||
<Button onClick={handleSubmit} disabled={submitting}>
|
||||
<Button onClick={handleSubmit} disabled={submitting || !trimmedComment}>
|
||||
{t('activation.submit')}
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -5,6 +5,6 @@ export function getActivationStatus() {
|
||||
return apiRequest<ActivationStatusDto>('/activation/status')
|
||||
}
|
||||
|
||||
export function requestActivation(comment: string | undefined) {
|
||||
return apiRequest<ActivationRequestDto>('/activation/request', { method: 'POST', body: { comment: comment ?? null } })
|
||||
export function requestActivation(comment: string) {
|
||||
return apiRequest<ActivationRequestDto>('/activation/request', { method: 'POST', body: { comment } })
|
||||
}
|
||||
|
||||
@@ -60,8 +60,10 @@ const resources = {
|
||||
activation: {
|
||||
title: 'Аккаунт не активирован',
|
||||
description:
|
||||
'Чтобы создавать конфиги, дождитесь активации администратором. Можно оставить комментарий к заявке.',
|
||||
commentLabel: 'Комментарий (необязательно)',
|
||||
'Чтобы создавать конфиги, дождитесь активации администратором. Обязательно напишите в комментарии, кто вы и откуда — иначе администратор не сможет вас опознать.',
|
||||
commentLabel: 'Комментарий (обязательно)',
|
||||
commentPlaceholder: 'Например: Никита, коллега Артёма с работы',
|
||||
commentHint: 'Укажите, кто вы и откуда: имя и как связаны с администратором.',
|
||||
submit: 'Запросить активацию',
|
||||
pending: 'Заявка на активацию отправлена, ожидайте решения администратора.',
|
||||
alreadyPending: 'У вас уже есть необработанная заявка на активацию.',
|
||||
@@ -652,8 +654,10 @@ const resources = {
|
||||
activation: {
|
||||
title: 'Account not activated',
|
||||
description:
|
||||
'Wait for an administrator to activate your account before creating configs. You can leave a comment with your request.',
|
||||
commentLabel: 'Comment (optional)',
|
||||
'Wait for an administrator to activate your account before creating configs. You must state in the comment who you are and where you are from — otherwise the administrator cannot identify you.',
|
||||
commentLabel: 'Comment (required)',
|
||||
commentPlaceholder: 'For example: Nikita, Artem’s coworker',
|
||||
commentHint: 'State who you are and where you are from: your name and how you know the administrator.',
|
||||
submit: 'Request activation',
|
||||
pending: 'Activation request sent, waiting for administrator review.',
|
||||
alreadyPending: 'You already have a pending activation request.',
|
||||
|
||||
Reference in New Issue
Block a user