fix: prevent Docker build failure when pnpm lockfile is missing (#130)
* fix: make docker build resilient when lockfile is absent * test: update e2e credentials for secure admin seed policy
This commit is contained in:
parent
90b712ea29
commit
2111f03542
|
|
@ -1,5 +1,5 @@
|
||||||
AUTH_USER=testadmin
|
AUTH_USER=testadmin
|
||||||
AUTH_PASS=testpass123
|
AUTH_PASS=testpass1234!
|
||||||
API_KEY=test-api-key-e2e-12345
|
API_KEY=test-api-key-e2e-12345
|
||||||
AUTH_SECRET=test-legacy-secret
|
AUTH_SECRET=test-legacy-secret
|
||||||
MC_ALLOW_ANY_HOST=1
|
MC_ALLOW_ANY_HOST=1
|
||||||
|
|
|
||||||
13
Dockerfile
13
Dockerfile
|
|
@ -3,14 +3,19 @@ RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json ./
|
||||||
|
COPY . .
|
||||||
# better-sqlite3 requires native compilation tools
|
# better-sqlite3 requires native compilation tools
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ --no-install-recommends && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ --no-install-recommends && rm -rf /var/lib/apt/lists/*
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN if [ -f pnpm-lock.yaml ]; then \
|
||||||
|
pnpm install --frozen-lockfile; \
|
||||||
|
else \
|
||||||
|
echo "WARN: pnpm-lock.yaml not found in build context; running non-frozen install"; \
|
||||||
|
pnpm install --no-frozen-lockfile; \
|
||||||
|
fi
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app ./
|
||||||
COPY . .
|
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
FROM node:20-slim AS runtime
|
FROM node:20-slim AS runtime
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,13 @@ rm -rf node_modules
|
||||||
pnpm install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### "pnpm-lock.yaml not found" during Docker build
|
||||||
|
|
||||||
|
If your deployment context omits `pnpm-lock.yaml`, Docker build now falls back to
|
||||||
|
`pnpm install --no-frozen-lockfile`.
|
||||||
|
|
||||||
|
For reproducible builds, include `pnpm-lock.yaml` in the build context.
|
||||||
|
|
||||||
### "Invalid ELF header" or "Mach-O" errors
|
### "Invalid ELF header" or "Mach-O" errors
|
||||||
|
|
||||||
The native binary was compiled on a different platform. Rebuild:
|
The native binary was compiled on a different platform. Rebuild:
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { test, expect } from '@playwright/test'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
test.describe('CSRF Origin Validation (Issue #20)', () => {
|
test.describe('CSRF Origin Validation (Issue #20)', () => {
|
||||||
|
const TEST_PASS = 'testpass1234!'
|
||||||
|
|
||||||
test('POST with mismatched Origin is rejected', async ({ request }) => {
|
test('POST with mismatched Origin is rejected', async ({ request }) => {
|
||||||
const res = await request.post('/api/auth/login', {
|
const res = await request.post('/api/auth/login', {
|
||||||
data: { username: 'test', password: 'test' },
|
data: { username: 'test', password: 'test' },
|
||||||
|
|
@ -21,7 +23,7 @@ test.describe('CSRF Origin Validation (Issue #20)', () => {
|
||||||
|
|
||||||
test('POST with matching Origin is allowed', async ({ request }) => {
|
test('POST with matching Origin is allowed', async ({ request }) => {
|
||||||
const res = await request.post('/api/auth/login', {
|
const res = await request.post('/api/auth/login', {
|
||||||
data: { username: 'testadmin', password: 'testpass123' },
|
data: { username: 'testadmin', password: TEST_PASS },
|
||||||
headers: {
|
headers: {
|
||||||
'origin': 'http://127.0.0.1:3005',
|
'origin': 'http://127.0.0.1:3005',
|
||||||
'host': '127.0.0.1:3005'
|
'host': '127.0.0.1:3005'
|
||||||
|
|
@ -33,7 +35,7 @@ test.describe('CSRF Origin Validation (Issue #20)', () => {
|
||||||
|
|
||||||
test('POST without Origin header is allowed (non-browser client)', async ({ request }) => {
|
test('POST without Origin header is allowed (non-browser client)', async ({ request }) => {
|
||||||
const res = await request.post('/api/auth/login', {
|
const res = await request.post('/api/auth/login', {
|
||||||
data: { username: 'testadmin', password: 'testpass123' },
|
data: { username: 'testadmin', password: TEST_PASS },
|
||||||
})
|
})
|
||||||
// No Origin = non-browser client, should be allowed through CSRF check
|
// No Origin = non-browser client, should be allowed through CSRF check
|
||||||
expect(res.status()).not.toBe(403)
|
expect(res.status()).not.toBe(403)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { test, expect } from '@playwright/test'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
test.describe('Login Flow', () => {
|
test.describe('Login Flow', () => {
|
||||||
|
const TEST_PASS = 'testpass1234!'
|
||||||
|
|
||||||
test('login page loads', async ({ page }) => {
|
test('login page loads', async ({ page }) => {
|
||||||
await page.goto('/login')
|
await page.goto('/login')
|
||||||
await expect(page).toHaveURL(/\/login/)
|
await expect(page).toHaveURL(/\/login/)
|
||||||
|
|
@ -18,7 +20,7 @@ test.describe('Login Flow', () => {
|
||||||
|
|
||||||
test('login API returns session cookie on success', async ({ request }) => {
|
test('login API returns session cookie on success', async ({ request }) => {
|
||||||
const res = await request.post('/api/auth/login', {
|
const res = await request.post('/api/auth/login', {
|
||||||
data: { username: 'testadmin', password: 'testpass123' },
|
data: { username: 'testadmin', password: TEST_PASS },
|
||||||
headers: { 'x-forwarded-for': '10.88.88.1' }
|
headers: { 'x-forwarded-for': '10.88.88.1' }
|
||||||
})
|
})
|
||||||
expect(res.status()).toBe(200)
|
expect(res.status()).toBe(200)
|
||||||
|
|
@ -39,7 +41,7 @@ test.describe('Login Flow', () => {
|
||||||
test('session cookie grants API access', async ({ request }) => {
|
test('session cookie grants API access', async ({ request }) => {
|
||||||
// Login to get a session
|
// Login to get a session
|
||||||
const loginRes = await request.post('/api/auth/login', {
|
const loginRes = await request.post('/api/auth/login', {
|
||||||
data: { username: 'testadmin', password: 'testpass123' },
|
data: { username: 'testadmin', password: TEST_PASS },
|
||||||
headers: { 'x-forwarded-for': '10.88.88.2' }
|
headers: { 'x-forwarded-for': '10.88.88.2' }
|
||||||
})
|
})
|
||||||
expect(loginRes.status()).toBe(200)
|
expect(loginRes.status()).toBe(200)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { test, expect } from '@playwright/test'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
test.describe('Login Rate Limiting (Issue #8)', () => {
|
test.describe('Login Rate Limiting (Issue #8)', () => {
|
||||||
|
const TEST_PASS = 'testpass1234!'
|
||||||
|
|
||||||
test('blocks login after 5 rapid failed attempts', async ({ request }) => {
|
test('blocks login after 5 rapid failed attempts', async ({ request }) => {
|
||||||
const results: number[] = []
|
const results: number[] = []
|
||||||
|
|
||||||
|
|
@ -25,7 +27,7 @@ test.describe('Login Rate Limiting (Issue #8)', () => {
|
||||||
|
|
||||||
test('successful login is not blocked for fresh IP', async ({ request }) => {
|
test('successful login is not blocked for fresh IP', async ({ request }) => {
|
||||||
const res = await request.post('/api/auth/login', {
|
const res = await request.post('/api/auth/login', {
|
||||||
data: { username: 'testadmin', password: 'testpass123' },
|
data: { username: 'testadmin', password: TEST_PASS },
|
||||||
headers: { 'x-real-ip': '10.88.88.88' }
|
headers: { 'x-real-ip': '10.88.88.88' }
|
||||||
})
|
})
|
||||||
// Should succeed (200) or at least not be rate limited
|
// Should succeed (200) or at least not be rate limited
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue