Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions hermes_cli/dashboard_auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,18 @@ async def auth_login(request: Request, provider: str, next: str = ""):
detail=f"Provider does not support interactive login: {provider!r}",
)

if getattr(p, "supports_password", False):
# Password-only providers do not have an OAuth redirect flow. Some
# native clients still open /auth/login?provider=basic as their generic
# "sign in" entrypoint; send them to the password login page instead of
# calling start_login() and returning a 500.
from urllib.parse import quote

safe_next = _validate_post_login_target(next)
prefix = _prefix(request)
suffix = f"?next={quote(safe_next, safe='')}" if safe_next else ""
return RedirectResponse(url=f"{prefix}/login{suffix}", status_code=302)

try:
ls = p.start_login(redirect_uri=_redirect_uri(request))
except ProviderError as e:
Expand Down
35 changes: 35 additions & 0 deletions tests/hermes_cli/test_dashboard_auth_password_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,41 @@ def test_oauth_provider_reports_false(self):
web_server.app.state.auth_required = prev


# ---------------------------------------------------------------------------
# /auth/login with password provider — native-client compatibility shim
# ---------------------------------------------------------------------------


class TestPasswordProviderAuthLoginRedirect:
def test_auth_login_redirects_password_provider_to_login_page(self, gated_app):
resp = gated_app.get(
"/auth/login?provider=testpw&next=/sessions",
follow_redirects=False,
)

assert resp.status_code == 302
assert resp.headers["location"] == "/login?next=%2Fsessions"

def test_auth_login_redirect_honours_forwarded_prefix(self, gated_app):
resp = gated_app.get(
"/auth/login?provider=testpw&next=/sessions",
headers={"x-forwarded-prefix": "/hermes"},
follow_redirects=False,
)

assert resp.status_code == 302
assert resp.headers["location"] == "/hermes/login?next=%2Fsessions"

def test_auth_login_redirect_drops_open_redirect_next(self, gated_app):
resp = gated_app.get(
"/auth/login?provider=testpw&next=https://evil.example/phish",
follow_redirects=False,
)

assert resp.status_code == 302
assert resp.headers["location"] == "/login"


# ---------------------------------------------------------------------------
# /auth/password-login — end-to-end through the real middleware
# ---------------------------------------------------------------------------
Expand Down
9 changes: 5 additions & 4 deletions ui-tui/src/app/usePet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useStdout } from '@hermes/ink'
import { useCallback, useEffect, useRef, useState } from 'react'

import type { PetGrid } from '../components/petSprite.js'
import { asRpcResult } from '../lib/rpc.js'

import { useGateway } from './gatewayContext.js'
import { $overlayState, getOverlayState } from './overlayStore.js'
Expand Down Expand Up @@ -60,7 +61,7 @@ interface KittyView {
placeholder: string[]
}

interface PetCellsResult {
interface PetCellsResult extends Record<string, unknown> {
color?: string
enabled?: boolean
frameMs?: number
Expand Down Expand Up @@ -107,7 +108,7 @@ export interface PetRender {
* live. The frame cache is keyed by `slug:state` so a switch re-pulls cleanly.
*/
export function usePet(): PetRender {
const { rpc } = useGateway()
const { gw } = useGateway()
const { write } = useStdout()
const [enabled, setEnabled] = useState(false)
const [grid, setGrid] = useState<PetGrid | null>(null)
Expand Down Expand Up @@ -194,7 +195,7 @@ export function usePet(): PetRender {
const sync = useCallback(
async (state: PetState) => {
try {
const res = (await rpc('pet.cells', { graphics: IS_TTY, state })) as PetCellsResult | null
const res = asRpcResult<PetCellsResult>(await gw.request<PetCellsResult>('pet.cells', { graphics: IS_TTY, state }))

if (!res) {
return
Expand Down Expand Up @@ -247,7 +248,7 @@ export function usePet(): PetRender {
// cosmetic — ignore RPC failures
}
},
[rpc, releaseKitty]
[gw, releaseKitty]
)

// Pull frames whenever the state changes (if not already cached for the
Expand Down