Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature+Fix] KeyCloak access to RM admin + bugfix +deps update #46

Merged
merged 7 commits into from
Feb 12, 2025
Merged
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
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ COPY . /app
ARG VITE_ASSET_SET=neutral
ENV VITE_ASSET_SET=$VITE_ASSET_SET

# Set release tag so we can show our deployment version to users
ARG VITE_RELEASE_TAG=Developing
ENV VITE_RELEASE_TAG=$VITE_RELEASE_TAG

RUN npm install \
&& chmod a+x /docker-entrypoint.sh \
&& npm run build \
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/assets/icons/kclogo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@
"manageUsersView.addUsers": "Add Users",
"manageUsersView.approveUsers": "Approve Users",
"manageUsersView.manageUsers": "Manage Users",
"manageUsersView.keycloakAccess": "Access Keycloak (Beta)",
"manageUsersView.keycloakAccessDesc1": "<strong>Keycloak</strong> is a backend service, which syncs user's roles and groups across services integrated to Deploy App. Managing groups is <strong>not</strong> necessary right now.",
"manageUsersView.keycloakAccessDesc2": "If you know how to use it, you may access it from the button below. Later on, user role & group management will be <strong>done directly here in Deploy App</strong> for maximum ease of use.",
"manageUsersView.openKeycloak": "Open Keycloak",

"USERMANAGEMENTVIEW": "USERMANAGEMENTVIEW",
"userManagement.rejectionSuccessMessage": "Success: The user has been removed.",
Expand Down
4 changes: 4 additions & 0 deletions src/assets/locale/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@
"manageUsersView.addUsers": "Lisää käyttäjiä",
"manageUsersView.approveUsers": "Hyväksy käyttäjiä",
"manageUsersView.manageUsers": "Hallitse käyttäjiä",
"manageUsersView.keycloakAccess": "Avaa Keycloak (Beta)",
"manageUsersView.keycloakAccessDesc1": "<strong>Keycloak</strong> on taustapalvelu, joka synkronoi käyttäjien roolit ja ryhmät Deploy App-palvelimessasi mukana olevien palveluiden välillä. Sen käyttäminen <strong>ei</strong> ole tällä hetkellä välttämätöntä.",
"manageUsersView.keycloakAccessDesc2": "Jos osaat käyttää Keycloakia, voit avata sen alla olevasta painikkeesta. Myöhemmin käyttäjien rooli- ja ryhmähallinta tehdään <strong>suoraan Deploy Appissa</strong>, jotta käyttökokemus olisi mahdollisimman helppo.",
"manageUsersView.openKeycloak": "Avaa Keycloak",

"USERMANAGEMENTVIEW": "USERMANAGEMENTVIEW",
"userManagement.rejectionSuccessMessage": "Käyttäjän hylkääminen onnistui.",
Expand Down
14 changes: 6 additions & 8 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { Trans, useTranslation } from "react-i18next";
import { InfoModal } from "./InfoModal";
import { DropdownMenu } from "./DropdownMenu";
import { useLanguageChange } from "./Localization/LanguageChange";
import useHealthCheck from "../hook/helpers/useHealthcheck";

export function Footer() {
// Use both common and dynamic namespaces
const { t } = useTranslation(["common", "dynamic"]);
const isMtls = window.location.origin.includes("mtls.");
const { version } = useHealthCheck();

// Read the deployment version from VITE_RELEASE_TAG
const deploymentVersion =
(import.meta.env.VITE_RELEASE_TAG as string) || t("footer.loading");

const feedbackLink = t("footer.feedbackForm", { ns: "dynamic" });
const { changeLanguage, availableLanguages } = useLanguageChange();

Expand All @@ -21,9 +23,8 @@ export function Footer() {
return (
<div className="font-heading text-uppercase text-center text-sm text-gray-500 pt-5 mt-10 mx-auto max-w-screen-xl">
<hr className="mx-auto" />

<div className="pt-4 py-3">
RASENMAEHER {version || <Trans i18nKey="footer.loading" />} -{" "}
Deploy App {deploymentVersion} -{" "}
<DropdownMenu
triggerLabel="Language"
items={languageItems}
Expand All @@ -36,9 +37,7 @@ export function Footer() {
- <Trans i18nKey="footer.authenticatedWithMtls" />
</div>
)}

<hr className="mx-auto w-56" />

<div className="py-5 text-xs">
<Trans i18nKey="footer.proudlyServedBy" />{" "}
<a
Expand Down Expand Up @@ -81,7 +80,6 @@ export function Footer() {
<Trans i18nKey="footer.feedbackFormText" ns="dynamic" />
</a>
</div>

<hr className="mx-auto w-56" />
</div>
);
Expand Down
36 changes: 25 additions & 11 deletions src/views/login/EnrollmentView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import QRCode from "react-qr-code";
import { useOwnEnrollmentStatus } from "../../hook/api/useOwnEnrollmentStatus";
import { useNavigate } from "react-router-dom";
Expand All @@ -16,30 +16,39 @@ export function EnrollmentView() {
const { isCopied, copyError, handleCopy } = useCopyToClipboard();
const callsign = localStorage.getItem("callsign") ?? undefined;
const approveCode = localStorage.getItem("approveCode") ?? undefined;

// Build the approval URL.
const protocol = window.location.protocol;
const host = window.location.host;
const approvalUrl = `${protocol}//mtls.${host}/app/admin/user-management/approval?callsign=${
callsign ?? ""
}&&approvalcode=${approveCode ?? ""}`;

// Redirect to login if approveCode or callsign is missing
// Local state to control polling behavior
const [shouldPoll, setShouldPoll] = useState(true);

// Redirect to login if approveCode or callsign is missing.
useEffect(() => {
if (!approveCode || !callsign) {
navigate("/login");
}
}, [approveCode, callsign, navigate]);

// Check enrollment status periodically and navigate on success
useOwnEnrollmentStatus({
onSuccess: (enrolled) => {
if (enrolled) {
navigate("/login/createmtls");
}
},
refetchInterval: 1000,
// Use the enrollment status hook.
const { data: enrolled, isLoading } = useOwnEnrollmentStatus({
refetchInterval: shouldPoll ? 5000 : false, // Stop polling once enrolled
});

// Render the waiting for approval view
// Effect to navigate on successful enrollment
useEffect(() => {
if (enrolled && shouldPoll) {
setShouldPoll(false); // Stop polling to prevent infinite re-renders

// Use hard redirect to force React to unmount the view
window.location.replace("/login/createmtls");
}
}, [enrolled, shouldPoll]);

return (
<Layout showNavbar={true} showFooter={false}>
<CardsContainer>
Expand Down Expand Up @@ -110,6 +119,11 @@ export function EnrollmentView() {
},
]}
/>
{isLoading && (
<div className="text-white mt-4">
{t("checking-enrollment-status")}
</div>
)}
</div>
</CardsContainer>
</Layout>
Expand Down
47 changes: 38 additions & 9 deletions src/views/login/LoginView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,40 +33,52 @@ export function LoginView() {
const { t } = useTranslation();
const navigate = useNavigate();
const params = useQueryParams();
// Retrieve the code from the URL (if provided)
const codeFromQuery = params.get("code");
const { setOtpVerified } = useContext(UserTypeContext);
const loginCodeStore = useLoginCodeStore();
const { deployment } = useHealthcheck();
const [codeNotValid, setCodeNotValid] = useState(false);
// Prevent auto-submit from running more than once.
const [hasAutoSubmitted, setHasAutoSubmitted] = useState(false);
const protocol = window.location.protocol;
const host = window.location.host;
const mtlsUrl = `${protocol}//mtls.${host}/app/admin/`;
const buttonStyle = "min-h-[70px]";

// Validation schema using Yup.
const CodeSchema = yup.object().shape({
code: yup
.string()
.required(t("login-code-required"))
.matches(TOKEN_REGEX, t("code-is-wrong")),
});

// If the user arrives with a code in the URL, we want to auto-validate.
// Otherwise, we disable auto-validation (and error messages) until submit.
const autoValidate = !!codeFromQuery;

const formik = useFormik({
initialValues: {
code: params.get("code")?.toUpperCase() ?? "",
code: codeFromQuery ? codeFromQuery.toUpperCase() : "",
},
validationSchema: CodeSchema,
validateOnMount: true,
// Only auto-validate if a code came in via the URL.
validateOnMount: autoValidate,
validateOnChange: autoValidate,
validateOnBlur: autoValidate,
onSubmit: (values) => {
checkCode(values.code);
},
});

// Destructure formik properties
const { values, isValid, submitForm, setFieldValue } = formik;
// Destructure useful Formik properties.
const { values, isValid, submitForm, setFieldValue, submitCount } = formik;

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const upperCaseValue = e.target.value.toUpperCase();
// Using void to intentionally ignore the Promise returned by setFieldValue, to suppress eslint
setCodeNotValid(false); // Clear error
formik.setErrors({}); // Reset all Formik errors when user starts typing
void setFieldValue("code", upperCaseValue, false);
};

Expand Down Expand Up @@ -101,12 +113,26 @@ export function LoginView() {
},
});

// Auto-submit only if a code is provided in the URL
useEffect(() => {
if (values.code && isValid && !isLoading && !hasAutoSubmitted) {
if (
codeFromQuery && // only auto-submit when arriving via URL
values.code &&
isValid &&
!isLoading &&
!hasAutoSubmitted
) {
setHasAutoSubmitted(true);
void submitForm();
}
}, [values.code, isValid, isLoading, hasAutoSubmitted, submitForm]);
}, [
codeFromQuery,
values.code,
isValid,
isLoading,
hasAutoSubmitted,
submitForm,
]);

return (
<Layout showNavbar={false} showFooter={false} showPublicFooter={true}>
Expand All @@ -131,7 +157,10 @@ export function LoginView() {
onChange={handleChange}
/>
<span className="text-red-500">
<ErrorMessage name="code" />
{/*
Show Formik's error only after the user has attempted a submission
*/}
{submitCount > 0 && <ErrorMessage name="code" />}
{codeNotValid && <div>{t("code-is-wrong")}</div>}
{isError && (
<div>
Expand All @@ -149,7 +178,7 @@ export function LoginView() {
width: "full",
}}
type="submit"
disabled={!isValid || isLoading}
disabled={!values.code || isLoading}
styling={buttonStyle}
>
<div className="flex items-center justify-center w-full h-full">
Expand Down
Loading