fix: remove hardcoded CORS_ORIGINS from docker-compose, improve login errors
CORS_ORIGINS was hardcoded in docker-compose.yml, silently overriding .env and breaking login for users who change the frontend port. It now comes from .env exclusively, with a clear comment in .env.example. Login page now distinguishes network errors (CORS/offline) from wrong credentials, and footer correctly references .env instead of config.yml.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# Backend - server-side only (NEVER commit .env)
|
# Backend - server-side only (NEVER commit .env)
|
||||||
SECRET_KEY=change_me_in_production
|
SECRET_KEY=change_me_in_production
|
||||||
SQLITE_PATH=./data/homelab.db
|
SQLITE_PATH=./data/homelab.db
|
||||||
|
# Set this to the URL(s) you use to access Homelable in your browser.
|
||||||
CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"]
|
CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"]
|
||||||
|
|
||||||
# Auth — default credentials: admin / admin
|
# Auth — default credentials: admin / admin
|
||||||
|
|||||||
+1
-2
@@ -7,9 +7,8 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
# Override env_file values that differ in Docker
|
# Override env_file: SQLite path must point inside the container volume
|
||||||
SQLITE_PATH: /app/data/homelab.db
|
SQLITE_PATH: /app/data/homelab.db
|
||||||
CORS_ORIGINS: '["http://localhost:3000"]'
|
|
||||||
volumes:
|
volumes:
|
||||||
- backend_data:/app/data
|
- backend_data:/app/data
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ export function LoginPage() {
|
|||||||
try {
|
try {
|
||||||
const res = await authApi.login(username, password)
|
const res = await authApi.login(username, password)
|
||||||
login(res.data.access_token)
|
login(res.data.access_token)
|
||||||
} catch {
|
} catch (err: unknown) {
|
||||||
setError('Invalid username or password')
|
const hasResponse = err && typeof err === 'object' && 'response' in err
|
||||||
|
setError(hasResponse ? 'Invalid username or password' : 'Could not reach the server — check your CORS_ORIGINS setting')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,7 @@ export function LoginPage() {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="text-center text-[10px] text-muted-foreground/40 mt-4">
|
<p className="text-center text-[10px] text-muted-foreground/40 mt-4">
|
||||||
Credentials configured in <span className="font-mono">config.yml</span>
|
Credentials configured in <span className="font-mono">.env</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ describe('LoginPage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows a generic error message — no credential enumeration', async () => {
|
it('shows a generic error message — no credential enumeration', async () => {
|
||||||
vi.mocked(authApi.login).mockRejectedValue(new Error('401'))
|
vi.mocked(authApi.login).mockRejectedValue({ response: { status: 401 } })
|
||||||
render(<LoginPage />)
|
render(<LoginPage />)
|
||||||
fireEvent.change(screen.getByLabelText('Username'), { target: { value: 'admin' } })
|
fireEvent.change(screen.getByLabelText('Username'), { target: { value: 'admin' } })
|
||||||
fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'wrongpass' } })
|
fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'wrongpass' } })
|
||||||
@@ -65,6 +65,17 @@ describe('LoginPage', () => {
|
|||||||
expect(errors[0].textContent).toBe('Invalid username or password')
|
expect(errors[0].textContent).toBe('Invalid username or password')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows a network error message when no response (e.g. CORS misconfiguration)', async () => {
|
||||||
|
vi.mocked(authApi.login).mockRejectedValue(new Error('Network Error'))
|
||||||
|
render(<LoginPage />)
|
||||||
|
fireEvent.change(screen.getByLabelText('Username'), { target: { value: 'admin' } })
|
||||||
|
fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'admin' } })
|
||||||
|
fireEvent.submit(screen.getByRole('button', { name: /sign in/i }).closest('form')!)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Could not reach the server/)).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('clears previous error before each new attempt', async () => {
|
it('clears previous error before each new attempt', async () => {
|
||||||
vi.mocked(authApi.login)
|
vi.mocked(authApi.login)
|
||||||
.mockRejectedValueOnce(new Error('401'))
|
.mockRejectedValueOnce(new Error('401'))
|
||||||
|
|||||||
Reference in New Issue
Block a user