diff --git a/Containerfile b/Containerfile index 433ecfe17..542683d44 100644 --- a/Containerfile +++ b/Containerfile @@ -12,13 +12,13 @@ ENV NODE_OPTIONS='--max-old-space-size=8192' RUN npm ci RUN npm run build -FROM registry.access.redhat.com/ubi9/go-toolset:1.23.9-1751538372 as proxy-build +FROM registry.access.redhat.com/ubi9/go-toolset:1.24.6-1762373805 as proxy-build WORKDIR /app COPY proxy /app USER 0 RUN CGO_ENABLED=1 CGO_CFLAGS=-flto GOEXPERIMENT=strictfipsruntime go build -FROM quay.io/flightctl/flightctl-base:9.6-1758714456 +FROM quay.io/flightctl/flightctl-base:9.6-1762316544 COPY --from=ui-build /app/apps/standalone/dist /app/proxy/dist COPY --from=proxy-build /app/flightctl-ui /app/proxy WORKDIR /app/proxy diff --git a/Containerfile.ocp b/Containerfile.ocp index d62b004a5..6d68bc148 100644 --- a/Containerfile.ocp +++ b/Containerfile.ocp @@ -12,13 +12,13 @@ ENV NODE_OPTIONS='--max-old-space-size=8192' RUN npm ci RUN npm run build:ocp -FROM registry.access.redhat.com/ubi9/go-toolset:1.23.9-1751538372 as proxy-build +FROM registry.access.redhat.com/ubi9/go-toolset:1.24.6-1762373805 as proxy-build WORKDIR /app COPY proxy /app USER 0 RUN CGO_ENABLED=1 CGO_CFLAGS=-flto GOEXPERIMENT=strictfipsruntime go build -FROM quay.io/flightctl/flightctl-base:9.6-1758714456 +FROM quay.io/flightctl/flightctl-base:9.6-1762316544 COPY --from=ui-build /app/apps/ocp-plugin/dist /app/proxy/dist COPY --from=proxy-build /app/flightctl-ui /app/proxy WORKDIR /app/proxy diff --git a/README.md b/README.md index 736e752dd..de16ea5b8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Monorepo containing UIs for [Flight Control](https://github.com/flightctl/flight ## Prerequisites -- `Git`, `Node.js v22.x`, `npm v10.x`, `rsync`, `go` (>= 1.23) +- `Git`, `Node.js v22.x`, `npm v10.x`, `rsync`, `go` (>= 1.24) ## Building diff --git a/apps/standalone/src/app/components/Login/LoginPage.tsx b/apps/standalone/src/app/components/Login/LoginPage.tsx new file mode 100644 index 000000000..9c78ebd78 --- /dev/null +++ b/apps/standalone/src/app/components/Login/LoginPage.tsx @@ -0,0 +1,190 @@ +import * as React from 'react'; +import { + Alert, + Bullseye, + Button, + Card, + CardBody, + CardFooter, + FormGroup, + FormHelperText, + FormSection, + HelperText, + HelperTextItem, + Stack, + StackItem, + Text, + TextArea, + TextContent, + TextVariants, + Title, +} from '@patternfly/react-core'; + +import { ORGANIZATION_STORAGE_KEY } from '@flightctl/ui-components/src/utils/organizationStorage'; +import FlightCtlForm from '@flightctl/ui-components/src/components/form/FlightCtlForm'; +import { useTranslation } from '@flightctl/ui-components/src/hooks/useTranslation'; +import { loginAPI } from '../../utils/apiCalls'; + +const EXPIRATION = 'expiration'; + +const nowInSeconds = () => Math.floor(Date.now() / 1000); + +// Simple JWT format validation - checks if token has 3 parts separated by dots +export const isValidJwtTokenFormat = (token: string): boolean => { + if (!token) return false; + const parts = token.split('.'); + if (parts.length !== 3) return false; + // Check that each part contains only valid base64url characters + const base64urlPattern = /^[A-Za-z0-9_-]+$/; + return parts.every((part) => part.length > 0 && base64urlPattern.test(part)); +}; + +export const LoginPage = () => { + const { t } = useTranslation(); + const [token, setToken] = React.useState(''); + const [validationError, setValidationError] = React.useState(''); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [submitError, setSubmitError] = React.useState(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitError(undefined); + setIsSubmitting(true); + + try { + localStorage.removeItem(EXPIRATION); + localStorage.removeItem(ORGANIZATION_STORAGE_KEY); + + const resp = await fetch(loginAPI, { + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + method: 'POST', + body: JSON.stringify({ + token: token.trim(), + }), + }); + + if (!resp.ok) { + let errorMessage = t('Authentication failed'); + try { + const contentType = resp.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + const errorData = (await resp.json()) as { error?: string }; + errorMessage = errorData.error || errorMessage; + } else { + // Fallback for non-JSON responses + const text = await resp.text(); + if (text) { + errorMessage = text; + } + } + } catch (parseErr) { + // If parsing fails, use default error message + errorMessage = t('Authentication failed'); + } + setSubmitError(errorMessage); + setIsSubmitting(false); + return; + } + + const expiration = (await resp.json()) as { expiresIn: number }; + if (expiration.expiresIn) { + const now = nowInSeconds(); + localStorage.setItem(EXPIRATION, `${now + expiration.expiresIn}`); + } + + // Redirect to home page after successful login + window.location.href = '/'; + } catch (err) { + setSubmitError(t('Failed to authenticate. Please check your token and try again.')); + setIsSubmitting(false); + } + }; + + return ( + + + + + + + {t('Enter your Kubernetes token')} + + + + + + {t('Enter your Kubernetes service account token to authenticate with the cluster.')} + + {t('You can find this token in your Kubernetes service account credentials.')} + + + + + + +