Skip to content
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
17 changes: 14 additions & 3 deletions galasa-ui/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@
"descriptionline1": "Ein Zugriffstoken ist ein eindeutiger geheimer Schlüssel, der von einem Client-Programm gehalten wird, um Zugriff auf den Galasa-Dienst zu erhalten.",
"descriptionline2": "Ein Token hat die gleichen Zugriffsrechte wie der Benutzer, der es erstellt hat.",
"deleteButtontext": "{count} ausgewählte Zugriffstoken löschen",
"error": "Fehler beim Abrufen der Token vom Galasa API-Server"
"error": "Fehler beim Abrufen der Token vom Galasa API-Server",
"warningDaysMaximumTitle": "Limit für Token-Ablaufwarnung angewendet",
"warningDaysMaximumSubtitle": "Der maximale Wert für service.tokens.lifespan.nearly.expired.warning.days beträgt 30 Tage."
},
"ResultsTablePageSizingSetting": {
"title": "Testergebnisabfrage",
Expand Down Expand Up @@ -117,11 +119,20 @@
"token_name": "Tokenname",
"token_name_helper_text": "Hilft beim Wiedererkennen Ihrer Token.",
"token_name_placeholder": "z.B. galasactl Zugriff auf Windows",
"token_lifespan": "Token-Lebensdauer",
"select_lifespan": "Token-Lebensdauer auswählen",
"custom_lifespan": "Benutzerdefiniert",
"custom_expiry_date": "Benutzerdefiniertes Ablaufdatum",
"custom_expiry_date_helper_text": "Wählen Sie ein Datum zwischen 1 und 365 Tagen ab heute",
"custom_expiry_date_invalid": "Das Ablaufdatum muss zwischen 1 und 365 Tagen ab heute liegen",
"error_requesting_token": "Fehler beim Anfordern des Tokens"
},
"TokenCard": {
"createdAt": "Erstellt am:",
"owner": "Inhaber"
"expires": "Läuft ab:",
"expired": "Abgelaufen:",
"owner": "Inhaber",
"nearlyExpiredWarning": "Ihr persönlicher Zugriffstoken läuft bald ab. Nach Ablauf des Tokens können Sie ihn nicht mehr verwenden, um diesen Galasa-Dienst zu kontaktieren."
},
"TokenDeleteModal": {
"modalHeading": "Zugriffstoken löschen",
Expand Down Expand Up @@ -592,4 +603,4 @@
"deleteMessage": "Die Abfrage \"{name}\" wurde erfolgreich gelöscht.",
"duplicateMessage": "Die Abfrage \"{name}\" wurde erfolgreich dupliziert."
}
}
}
15 changes: 13 additions & 2 deletions galasa-ui/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@
"descriptionline1": "An access token is a unique secret key held by a client program so it has permission to use the Galasa service.",
"descriptionline2": "A token has the same access rights as the user who allocated it.",
"deleteButtontext": "Delete {count} selected access token",
"error": "Failed to fetch tokens from the Galasa API server"
"error": "Failed to fetch tokens from the Galasa API server",
"warningDaysMaximumTitle": "Token expiry warning days limit applied",
"warningDaysMaximumSubtitle": "The maximum value for service.tokens.lifespan.nearly.expired.warning.days is 30 days."
},
"ResultsTablePageSizingSetting": {
"title": "Test Run Query Results",
Expand Down Expand Up @@ -117,11 +119,20 @@
"token_name": "Token name",
"token_name_helper_text": "Use this to distinguish between your tokens in the future.",
"token_name_placeholder": "e.g. galasactl access for my Windows machine",
"token_lifespan": "Token lifespan",
"select_lifespan": "Select token lifespan",
"custom_lifespan": "Custom",
"custom_expiry_date": "Custom expiry date",
"custom_expiry_date_helper_text": "Select a date between 1 and 365 days from today",
"custom_expiry_date_invalid": "Expiry date must be between 1 and 365 days from today",
"error_requesting_token": "Error requesting access token"
},
"TokenCard": {
"createdAt": "Created at:",
"owner": "Owner"
"expires": "Expires:",
"expired": "Expired:",
"owner": "Owner",
"nearlyExpiredWarning": "Your personal access token will expire soon. Once it expires, you will no longer be able to use it to contact this Galasa service."
},
"TokenDeleteModal": {
"modalHeading": "Delete Access Tokens",
Expand Down
6 changes: 5 additions & 1 deletion galasa-ui/src/app/auth/tokens/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const dynamic = 'force-dynamic';

interface TokenDetails {
tokenDescription: string;
tokenLifespanDays: number;
}

// POST request handler for requests to /auth/tokens
Expand All @@ -33,11 +34,14 @@ export async function POST(request: NextRequest) {
// Store the client ID to be displayed to the user later
cookiesStore.set(AuthCookies.CLIENT_ID, clientId, { httpOnly: true });

// Store the token description to be passed to the API server on the callback
// Store the token description and lifespan to be passed to the API server on the callback
const requestBody: TokenDetails = await request.json();
cookiesStore.set(AuthCookies.TOKEN_DESCRIPTION, requestBody.tokenDescription, {
httpOnly: true,
});
cookiesStore.set(AuthCookies.TOKEN_LIFESPAN_DAYS, String(requestBody.tokenLifespanDays), {
httpOnly: true,
});

// Authenticate with the created client to get a new refresh token for this client
const authResponse = await sendAuthRequest(clientId);
Expand Down
9 changes: 8 additions & 1 deletion galasa-ui/src/app/mysettings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { cookies } from 'next/headers';
import AccessTokensSection from '@/components/mysettings/AccessTokensSection';
import TokenResponseModal from '@/components/tokens/TokenResponseModal';
import PageTile from '@/components/PageTile';
import { UsersAPIApi } from '@/generated/galasaapi';
import { ConfigurationPropertyStoreAPIApi, UsersAPIApi } from '@/generated/galasaapi';
import { createAuthenticatedApiConfiguration } from '@/utils/api';
import * as Constants from '@/utils/constants/common';
import BreadCrumb from '@/components/common/BreadCrumb';
Expand All @@ -20,6 +20,8 @@ import { fetchUserFromApiServer } from '@/actions/userServerActions';
import ProfileRole from '@/components/users/UserRole';
import DateTimeSettings from '@/components/mysettings/DateTimeSettings';
import ResultsTablePageSizeSetting from '@/components/mysettings/ResultsTablePageSizeSetting';
import { fetchTokenExpiryWarningConfiguration } from '@/utils/tokenExpiryWarning';

export default async function MySettings() {
const apiConfig = createAuthenticatedApiConfiguration();

Expand Down Expand Up @@ -55,6 +57,9 @@ export default async function MySettings() {
return <ErrorPage />;
}

const cpsApiClient = new ConfigurationPropertyStoreAPIApi(apiConfig);
const tokenExpiryWarningConfiguration = await fetchTokenExpiryWarningConfiguration(cpsApiClient);

return (
<main id="content">
<BreadCrumb breadCrumbItems={[HOME]} />
Expand All @@ -63,6 +68,8 @@ export default async function MySettings() {
<AccessTokensSection
accessTokensPromise={fetchAccessTokens(userLoginId)}
isAddBtnVisible={true}
tokenExpiryWarningDays={tokenExpiryWarningConfiguration.warningDays}
showMaxWarningDaysNotice={tokenExpiryWarningConfiguration.exceededMaximum}
/>
<TokenResponseModal refreshToken={refreshToken} clientId={clientId} onLoad={deleteCookies} />
<DateTimeSettings />
Expand Down
5 changes: 5 additions & 0 deletions galasa-ui/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export default async function HomePage() {
};
}
}

// If no content was found in CPS, throw an error to trigger fallback
if (!content.markdownContent) {
throw new Error('No markdown content found in CPS property');
}
} catch (error: any) {
console.warn('Failed to fetch custom markdown content from CPS', error);

Expand Down
13 changes: 12 additions & 1 deletion galasa-ui/src/app/users/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
*/
import PageTile from '@/components/PageTile';
import UserRoleSection from '@/components/users/UserRoleSection';
import { RBACRole, RoleBasedAccessControlAPIApi } from '@/generated/galasaapi';
import {
ConfigurationPropertyStoreAPIApi,
RBACRole,
RoleBasedAccessControlAPIApi,
} from '@/generated/galasaapi';
import { createAuthenticatedApiConfiguration } from '@/utils/api';
import AccessTokensSection from '@/components/mysettings/AccessTokensSection';
import { fetchAccessTokens } from '@/actions/getUserAccessTokens';
import { fetchUserFromApiServer } from '@/actions/userServerActions';
import BreadCrumb from '@/components/common/BreadCrumb';
import { EDIT_USER, HOME } from '@/utils/constants/breadcrumb';
import { fetchTokenExpiryWarningConfiguration } from '@/utils/tokenExpiryWarning';

// In order to extract query param on server-side
type UsersPageProps = {
Expand All @@ -37,6 +42,10 @@ export default async function EditUserPage(props: UsersPageProps) {

return roles;
};

const cpsApiClient = new ConfigurationPropertyStoreAPIApi(apiConfig);
const tokenExpiryWarningConfiguration = await fetchTokenExpiryWarningConfiguration(cpsApiClient);

return (
<main id="content">
<BreadCrumb breadCrumbItems={[HOME, EDIT_USER]} />
Expand All @@ -48,6 +57,8 @@ export default async function EditUserPage(props: UsersPageProps) {
<AccessTokensSection
accessTokensPromise={fetchAccessTokens(loginIdFromQueryParam)}
isAddBtnVisible={false}
tokenExpiryWarningDays={tokenExpiryWarningConfiguration.warningDays}
showMaxWarningDaysNotice={tokenExpiryWarningConfiguration.exceededMaximum}
/>
</main>
);
Expand Down
34 changes: 32 additions & 2 deletions galasa-ui/src/components/mysettings/AccessTokensSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
'use client';

import { useEffect, useState } from 'react';
import { Loading, Button } from '@carbon/react';
import { Loading, Button, InlineNotification } from '@carbon/react';
import styles from '@/styles/mysettings/MySettings.module.css';
import TokenCard from '@/components/tokens/TokenCard';
import ErrorPage from '@/app/error/page';
Expand All @@ -18,11 +18,15 @@ import { useTranslations } from 'next-intl';
interface AccessTokensSectionProps {
accessTokensPromise: Promise<AuthTokens | undefined>;
isAddBtnVisible: boolean;
tokenExpiryWarningDays: number;
showMaxWarningDaysNotice: boolean;
}

export default function AccessTokensSection({
accessTokensPromise,
isAddBtnVisible,
tokenExpiryWarningDays,
showMaxWarningDaysNotice,
}: AccessTokensSectionProps) {
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
Expand Down Expand Up @@ -72,13 +76,27 @@ export default function AccessTokensSection({

try {
const accessTokens = await accessTokensPromise;

if (accessTokens && accessTokens.tokens) {
setTokens(new Set(accessTokens.tokens));
} else {
throw new Error(translations('error'));
}
} catch (err) {
setIsError(true);
if (err instanceof Error && err.message === translations('error')) {
setIsError(true);
} else {
try {
const accessTokens = await accessTokensPromise;
if (accessTokens && accessTokens.tokens) {
setTokens(new Set(accessTokens.tokens));
} else {
throw new Error(translations('error'));
}
} catch (accessTokenError) {
setIsError(true);
}
}
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -107,6 +125,17 @@ export default function AccessTokensSection({
</div>
</div>

{showMaxWarningDaysNotice && (
<InlineNotification
kind="warning"
lowContrast
hideCloseButton
title={translations('warningDaysMaximumTitle')}
subtitle={translations('warningDaysMaximumSubtitle')}
className={styles.warningNotification}
/>
)}

<div className={styles.btnContainer}>
{/* // Only the user who is logged in can create a new token
// Admins can only delete users' tokens. */}
Expand All @@ -128,6 +157,7 @@ export default function AccessTokensSection({
key={token.tokenId}
token={token}
selectTokenForDeletion={selectTokenForDeletion}
expiryWarningDays={tokenExpiryWarningDays}
/>
))}
</div>
Expand Down
40 changes: 36 additions & 4 deletions galasa-ui/src/components/tokens/TokenCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,71 @@ import { AuthToken } from '@/generated/galasaapi';
import { useTranslations } from 'next-intl';
import { useDateTimeFormat } from '@/contexts/DateTimeFormatContext';
import { useMemo } from 'react';
import { DAY_MS } from '@/utils/constants/common';

function TokenCard({
token,
selectTokenForDeletion,
expiryWarningDays,
}: {
token: AuthToken;
selectTokenForDeletion: Function;
expiryWarningDays: number;
}) {
const translations = useTranslations('TokenCard');
const { formatDate } = useDateTimeFormat();
const formattedDate = useMemo(() => {
const formattedCreationDate = useMemo(() => {
return formatDate(new Date(token.creationTime!));
}, [token.creationTime, formatDate]);
const formattedExpiryDate = useMemo(() => {
return token.expiryTime ? formatDate(new Date(token.expiryTime)) : null;
}, [token.expiryTime, formatDate]);

// Check if token is expired
const isExpired = useMemo(() => {
if (!token.expiryTime) return false;
Comment thread
James-Cocker marked this conversation as resolved.
return new Date(token.expiryTime) < new Date();
}, [token.expiryTime]);

const isNearlyExpired = useMemo(() => {
if (!token.expiryTime || isExpired) return false;

const expiryTime = new Date(token.expiryTime).getTime();
const currentTime = Date.now();
const warningWindowInMilliseconds = expiryWarningDays * DAY_MS;

return expiryTime - currentTime <= warningWindowInMilliseconds;
}, [token.expiryTime, isExpired, expiryWarningDays]);

return (
<SelectableTile
onClick={() => selectTokenForDeletion(token.tokenId)}
value={true}
key={token.tokenId}
className={styles.cardContainer}
className={`${styles.cardContainer} ${isExpired ? styles.cardContainerExpired : ''}`}
>
<h5>{token.description}</h5>

<div className={styles.infoContainer}>
<h6>
{translations('createdAt')} {formattedDate}
{translations('createdAt')} {formattedCreationDate}
</h6>
{formattedExpiryDate && (
<h6 className={isExpired ? styles.expiredLabel : ''}>
{isExpired ? translations('expired') : translations('expires')} {formattedExpiryDate}
</h6>
)}
<h6>
{translations('owner')} {token.owner?.loginId}
</h6>
</div>

<Password className={styles.icon} size={40} />
<div className={styles.iconWarningContainer}>
<Password className={styles.icon} size={40} />
{isNearlyExpired && (
<p className={styles.expiryWarning}>{translations('nearlyExpiredWarning')}</p>
)}
</div>
</SelectableTile>
);
}
Expand Down
Loading
Loading