Compare commits
2 commits
fb133f9cba
...
2fc2bdcabb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fc2bdcabb | ||
|
|
bcfe3a2a15 |
6 changed files with 31 additions and 6 deletions
|
|
@ -128,6 +128,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Rate limiting
|
# Rate limiting
|
||||||
|
limiter.enabled = settings.rate_limit_enabled
|
||||||
app.state.limiter = limiter
|
app.state.limiter = limiter
|
||||||
|
|
||||||
@app.exception_handler(RateLimitExceeded)
|
@app.exception_handler(RateLimitExceeded)
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ class Settings(BaseSettings):
|
||||||
# Magic links
|
# Magic links
|
||||||
invite_ttl: int = 86400 # seconds
|
invite_ttl: int = 86400 # seconds
|
||||||
|
|
||||||
|
# Rate limiting (disable for e2e/load tests that authenticate repeatedly)
|
||||||
|
rate_limit_enabled: bool = True
|
||||||
|
|
||||||
# Signing keys
|
# Signing keys
|
||||||
signing_key_path: str = "data/keys"
|
signing_key_path: str = "data/keys"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No password set.</p>
|
<p>No password set.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form hx-post="/manage/credentials/password" hx-target="#password-section" hx-swap="innerHTML">
|
<div id="password-error"></div>
|
||||||
|
<form hx-post="/manage/credentials/password" hx-target="#password-error" hx-swap="innerHTML">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token_processor(request) }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token_processor(request) }}">
|
||||||
{% if has_password %}
|
{% if has_password %}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ test.describe('Credentials page', () => {
|
||||||
|
|
||||||
test.describe('Password validation', () => {
|
test.describe('Password validation', () => {
|
||||||
test('shows mismatch error', async ({ page }) => {
|
test('shows mismatch error', async ({ page }) => {
|
||||||
|
await page.fill('#current_password', fixtures.cred_password);
|
||||||
await page.fill('#password', 'newpassword1');
|
await page.fill('#password', 'newpassword1');
|
||||||
await page.fill('#confirm', 'newpassword2');
|
await page.fill('#confirm', 'newpassword2');
|
||||||
await page.click('#password-section button[type="submit"]');
|
await page.click('#password-section button[type="submit"]');
|
||||||
|
|
@ -51,6 +52,23 @@ test.describe('Credentials page', () => {
|
||||||
await expect(alert).toContainText('do not match');
|
await expect(alert).toContainText('do not match');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('keeps the password form visible after a validation error', async ({ page }) => {
|
||||||
|
await page.fill('#current_password', fixtures.cred_password);
|
||||||
|
await page.fill('#password', 'newpassword1');
|
||||||
|
await page.fill('#confirm', 'newpassword2');
|
||||||
|
await page.click('#password-section button[type="submit"]');
|
||||||
|
|
||||||
|
const alert = page.locator('#password-section [role="alert"]');
|
||||||
|
await expect(alert).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
|
// Regression: the form and its inputs must NOT disappear on error.
|
||||||
|
await expect(page.locator('#password')).toBeVisible();
|
||||||
|
await expect(page.locator('#confirm')).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.locator('#password-section button[type="submit"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test('password input has minlength="8"', async ({ page }) => {
|
test('password input has minlength="8"', async ({ page }) => {
|
||||||
await expect(page.locator('#password')).toHaveAttribute('minlength', '8');
|
await expect(page.locator('#password')).toHaveAttribute('minlength', '8');
|
||||||
});
|
});
|
||||||
|
|
@ -62,8 +80,9 @@ test.describe('Credentials page', () => {
|
||||||
|
|
||||||
test.describe('Password change', () => {
|
test.describe('Password change', () => {
|
||||||
test('succeeds with matching passwords', async ({ page }) => {
|
test('succeeds with matching passwords', async ({ page }) => {
|
||||||
await page.fill('#password', 'newpassword123');
|
await page.fill('#current_password', fixtures.cred_password);
|
||||||
await page.fill('#confirm', 'newpassword123');
|
await page.fill('#password', 'purple-tiger-mountain-42');
|
||||||
|
await page.fill('#confirm', 'purple-tiger-mountain-42');
|
||||||
await page.click('#password-section button[type="submit"]');
|
await page.click('#password-section button[type="submit"]');
|
||||||
|
|
||||||
const status = page.locator('#password-section [role="status"]');
|
const status = page.locator('#password-section [role="status"]');
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ test.describe('Full user journey', () => {
|
||||||
await expect(passwordInput).toBeVisible();
|
await expect(passwordInput).toBeVisible();
|
||||||
await expect(confirmInput).toBeVisible();
|
await expect(confirmInput).toBeVisible();
|
||||||
|
|
||||||
await passwordInput.fill('mypassword123');
|
await passwordInput.fill('purple-tiger-mountain-42');
|
||||||
await confirmInput.fill('mypassword123');
|
await confirmInput.fill('purple-tiger-mountain-42');
|
||||||
await page.click('#password-section button[type="submit"]');
|
await page.click('#password-section button[type="submit"]');
|
||||||
|
|
||||||
// Wait for success message
|
// Wait for success message
|
||||||
|
|
@ -51,7 +51,7 @@ test.describe('Full user journey', () => {
|
||||||
|
|
||||||
// ---- Step 4: Login with the password we just set ----
|
// ---- Step 4: Login with the password we just set ----
|
||||||
await page.fill('#username', fixtures.register_username);
|
await page.fill('#username', fixtures.register_username);
|
||||||
await page.fill('#password', 'mypassword123');
|
await page.fill('#password', 'purple-tiger-mountain-42');
|
||||||
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
||||||
|
|
||||||
// Wait for redirect to credentials page
|
// Wait for redirect to credentials page
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ echo "Starting Porchlight on port ${PORT}..."
|
||||||
echo " DB: ${OIDC_OP_SQLITE_PATH}"
|
echo " DB: ${OIDC_OP_SQLITE_PATH}"
|
||||||
OIDC_OP_ISSUER="${TARGET_URL}" \
|
OIDC_OP_ISSUER="${TARGET_URL}" \
|
||||||
OIDC_OP_DEBUG=true \
|
OIDC_OP_DEBUG=true \
|
||||||
|
OIDC_OP_RATE_LIMIT_ENABLED=false \
|
||||||
uv run --directory "$PROJECT_ROOT" \
|
uv run --directory "$PROJECT_ROOT" \
|
||||||
uvicorn porchlight.app:create_app \
|
uvicorn porchlight.app:create_app \
|
||||||
--factory --host 127.0.0.1 --port "$PORT" \
|
--factory --host 127.0.0.1 --port "$PORT" \
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue