Compare commits
1
Commits
16f94293ab
...
fc1fbcc762
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc1fbcc762 |
@@ -29,6 +29,9 @@ APP_ALLOW_REGISTRATION=true
|
||||
# to 0 for local development, so links can be resent immediately.
|
||||
MAGIC_LINK_RESEND_SECONDS=60
|
||||
|
||||
# Maximum number of projects a single user may create. 0 means unlimited.
|
||||
MAX_PROJECTS_PER_OWNER=0
|
||||
|
||||
# Base URL the app is reached at. Verification magic links point here, e.g.
|
||||
# <APP_URL>/verify-email?token=... The Docker image serves the SPA and the API
|
||||
# together on http://localhost:8080; a host `npm run dev` serves it on :5173.
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ All routes below require `Authorization: Bearer <jwt>`. A project belongs to one
|
||||
| `PATCH` | `/api/projects/{id}` | rename the project (`title`) |
|
||||
| `DELETE` | `/api/projects/{id}` | delete the project and its cards (`204`) |
|
||||
|
||||
`GET /api/projects` is always ordered alphabetically (case-insensitive) by title; there is no other sort option. A user may own at most **100 projects** — creating one beyond that responds `409`.
|
||||
`GET /api/projects` is always ordered alphabetically (case-insensitive) by title; there is no other sort option. A user may own at most `MAX_PROJECTS_PER_OWNER` projects (default: unlimited) — creating one beyond that responds `409`.
|
||||
|
||||
Create/update body: `title` (required, 1–255 chars).
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ All settings are optional environment variables (read from `.env` or the real en
|
||||
| `JWT_TTL` | `86400` | Token lifetime in seconds |
|
||||
| `APP_ALLOW_REGISTRATION` | `true` | When `false`, a magic link is only ever sent to an existing address — an unknown one is silently ignored, so no new accounts get created |
|
||||
| `MAGIC_LINK_RESEND_SECONDS` | `60` | Minimum gap before a magic link can be resent to the same address (sign-in or email-change). Docker Compose overrides this to `0`, so links resend immediately in development |
|
||||
| `MAX_PROJECTS_PER_OWNER` | `0` | Maximum number of projects a single user may create. `0` means unlimited |
|
||||
| `APP_URL` | `http://localhost:8080` | Base URL used to build magic links (`http://localhost:5173` for a host `npm run dev`) |
|
||||
| `WEBAUTHN_RP_ID` | `APP_URL`'s host | Passkey relying party ID (domain). Must be `localhost` or a real domain over HTTPS — a LAN IP won't work |
|
||||
| `WEBAUTHN_RP_NAME` | `Projects` | Passkey relying party display name, shown in the browser/OS prompt |
|
||||
|
||||
@@ -18,11 +18,12 @@ use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
final class ProjectController extends ProjectScopedController
|
||||
{
|
||||
private const TITLE_MAX = 255;
|
||||
private const MAX_PROJECTS_PER_OWNER = 100;
|
||||
|
||||
public function __construct(
|
||||
ProjectRepository $projects,
|
||||
private readonly CardStatusRepository $statuses,
|
||||
/** Maximum number of projects a single owner may create. 0 means unlimited. */
|
||||
private readonly int $maxProjectsPerOwner,
|
||||
) {
|
||||
parent::__construct($projects);
|
||||
}
|
||||
@@ -48,9 +49,9 @@ final class ProjectController extends ProjectScopedController
|
||||
$title = $validator->requiredString('title', self::TITLE_MAX);
|
||||
$validator->assert();
|
||||
|
||||
if ($this->projects->countForOwner($ownerId) >= self::MAX_PROJECTS_PER_OWNER) {
|
||||
if ($this->maxProjectsPerOwner > 0 && $this->projects->countForOwner($ownerId) >= $this->maxProjectsPerOwner) {
|
||||
throw new ApiException(
|
||||
sprintf('You have reached the maximum of %d projects.', self::MAX_PROJECTS_PER_OWNER),
|
||||
sprintf('You have reached the maximum of %d projects.', $this->maxProjectsPerOwner),
|
||||
409,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ final class Config
|
||||
public readonly string $webauthnRpName,
|
||||
/** Minimum gap between magic links sent to the same address. */
|
||||
public readonly int $resendIntervalSeconds,
|
||||
/** Maximum number of projects a single owner may create. 0 means unlimited. */
|
||||
public readonly int $maxProjectsPerOwner,
|
||||
public readonly MailConfig $mail,
|
||||
) {
|
||||
}
|
||||
@@ -59,6 +61,7 @@ final class Config
|
||||
$webauthnRpName = self::env('WEBAUTHN_RP_NAME', 'Projects');
|
||||
|
||||
$resendIntervalSeconds = (int) (self::env('MAGIC_LINK_RESEND_SECONDS') ?? '60');
|
||||
$maxProjectsPerOwner = (int) (self::env('MAX_PROJECTS_PER_OWNER') ?? '0');
|
||||
|
||||
$mailLogPath = self::env('MAIL_LOG_PATH', $storagePath . '/mail.log');
|
||||
if (!self::isAbsolutePath($mailLogPath)) {
|
||||
@@ -87,6 +90,7 @@ final class Config
|
||||
$webauthnRpId,
|
||||
$webauthnRpName,
|
||||
$resendIntervalSeconds,
|
||||
$maxProjectsPerOwner,
|
||||
$mail,
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ $webAuthn = new WebAuthn($config->webauthnRpName, $config->webauthnRpId, ['none'
|
||||
|
||||
$authController = new AuthController($users, $session, $verifier, $config->allowRegistration);
|
||||
$emailController = new EmailVerificationController($users, $verificationTokens, $verifier, $session);
|
||||
$projectController = new ProjectController($projects, $cardStatuses);
|
||||
$projectController = new ProjectController($projects, $cardStatuses, $config->maxProjectsPerOwner);
|
||||
$cardController = new CardController($projects, $cards, $cardStatuses);
|
||||
$cardStatusController = new CardStatusController($projects, $cardStatuses, $cards);
|
||||
$passkeyController = new PasskeyController($webAuthn, $passkeys, $webauthnChallenges, $users, $session);
|
||||
|
||||
@@ -39,8 +39,9 @@ final class ProjectTest extends ApiTestCase
|
||||
self::assertSame(['apple', 'Banana', 'Cherry'], $titles);
|
||||
}
|
||||
|
||||
public function test_an_owner_cannot_exceed_100_projects(): void
|
||||
public function test_an_owner_cannot_exceed_the_configured_project_limit(): void
|
||||
{
|
||||
$this->reconfigure(['MAX_PROJECTS_PER_OWNER' => '100']);
|
||||
$auth = $this->authHeader();
|
||||
|
||||
for ($i = 1; $i <= 100; $i++) {
|
||||
|
||||
Reference in New Issue
Block a user