Skip to content
5 changes: 5 additions & 0 deletions src/common/Pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Project from 'src/pages/Dashboard/Project';
import InsightsPage from 'src/pages/Insights';
import LoginPage from 'src/pages/LoginPage';
import Manual from 'src/pages/Manual';
import MediaPage from 'src/pages/Media';
import MediaNotFound from 'src/pages/NotFound/MediaNotFound';
import NotFound from 'src/pages/NotFound/NotFound';
import Plan from 'src/pages/Plan';
Expand Down Expand Up @@ -128,6 +129,10 @@ const Pages = () => {
path={`/${langPrefix}/media/oops`}
element={<MediaNotFound />}
/>
<Route
path={`/${langPrefix}/media/:id`}
element={<MediaPage />}
/>
<Route path={`/${langPrefix}/oops`} element={<NotFound />} />
<Route index element={<Dashboard />} />
<Route path={`/${langPrefix}/profile`} element={<Profile />} />
Expand Down
17 changes: 12 additions & 5 deletions src/common/components/BugDetail/Description.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Editor, MD, TextDescription } from '@appquality/unguess-design-system';
import { useTranslation } from 'react-i18next';
import { Bug } from 'src/features/api';
import styled from 'styled-components';
import { useTranslation } from 'react-i18next';
import { WrappedText } from 'src/common/components/WrappedText';

const Container = styled.div`
display: inline-block;
Expand Down Expand Up @@ -40,23 +39,31 @@ export default ({
<StyledLabel>
{t('__BUGS_PAGE_BUG_DETAIL_DESCRIPTION_LABEL')}
</StyledLabel>
<Editor contentType="markdown" editable={false}>
<Editor key={bug.step_by_step} contentType="markdown" editable={false}>
{bug.step_by_step}
</Editor>
</TextBlock>
<TextBlock>
<StyledLabel>
{t('__BUGS_PAGE_BUG_DETAIL_EXPECTED_RESULT_LABEL')}
</StyledLabel>
<Editor contentType="markdown" editable={false}>
<Editor
key={bug.expected_result}
contentType="markdown"
editable={false}
>
{bug.expected_result}
</Editor>
</TextBlock>
<TextBlock>
<StyledLabel>
{t('__BUGS_PAGE_BUG_DETAIL_CURRENT_RESULT_LABEL')}
</StyledLabel>
<Editor contentType="markdown" editable={false}>
<Editor
key={bug.current_result}
contentType="markdown"
editable={false}
>
{bug.current_result}
</Editor>
</TextBlock>
Expand Down
28 changes: 28 additions & 0 deletions src/common/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,14 @@ export interface paths {
};
};
};
"/signedMedia/{id}": {
get: operations["get-signedMedia-id"];
parameters: {
path: {
id: string;
};
};
};
"/plans/{pid}": {
/** */
get: operations["get-workspaces-wid-plans-pid"];
Expand Down Expand Up @@ -3596,6 +3604,26 @@ export interface operations {
302: never;
};
};
/** Returns a short-lived presigned S3 URL for a media item if the caller has access. Returns 403 otherwise. Public bugs (unexpired wp_appq_bug_link) bypass auth. */
"get-signedMedia-id": {
parameters: {
path: {
id: string;
};
};
responses: {
/** OK */
200: {
content: {
"application/json": {
url: string;
};
};
};
/** Forbidden */
403: unknown;
};
};
/** */
"get-workspaces-wid-plans-pid": {
parameters: {
Expand Down
13 changes: 13 additions & 0 deletions src/features/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,12 @@ const injectedRtkApi = api.injectEndpoints({
getMediaById: build.query<GetMediaByIdApiResponse, GetMediaByIdApiArg>({
query: (queryArg) => ({ url: `/media/${queryArg.id}` }),
}),
getSignedMediaById: build.query<
GetSignedMediaByIdApiResponse,
GetSignedMediaByIdApiArg
>({
query: (queryArg) => ({ url: `/signedMedia/${queryArg.id}` }),
}),
deletePlansByPid: build.mutation<
DeletePlansByPidApiResponse,
DeletePlansByPidApiArg
Expand Down Expand Up @@ -1748,6 +1754,12 @@ export type GetMediaByIdApiResponse = unknown;
export type GetMediaByIdApiArg = {
id: string;
};
export type GetSignedMediaByIdApiResponse = /** status 200 OK */ {
url: string;
};
export type GetSignedMediaByIdApiArg = {
id: string;
};
export type DeletePlansByPidApiResponse = unknown;
export type DeletePlansByPidApiArg = {
pid: string;
Expand Down Expand Up @@ -3468,6 +3480,7 @@ export const {
useGetInvitesByProfileAndTokenQuery,
useDeleteMediaCommentByMcidMutation,
useGetMediaByIdQuery,
useGetSignedMediaByIdQuery,
useDeletePlansByPidMutation,
useGetPlansByPidQuery,
usePatchPlansByPidMutation,
Expand Down
77 changes: 10 additions & 67 deletions src/pages/LoginPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@ import { FormikHelpers } from 'formik';
import { useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import WPAPI from 'src/common/wpapi';
import { AuthCardWrapper } from 'src/common/components/AuthCardWrapper';
import { Track } from 'src/common/Track';
import { useGetUsersMeQuery } from 'src/features/api';
import { useAuth } from 'src/features/auth/context';
import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute';
import styled from 'styled-components';
import { AuthCardWrapper } from 'src/common/components/AuthCardWrapper';
import { Track } from 'src/common/Track';
import { LoginForm } from './LoginForm';
import { LoginFormFields } from './type';
import { AuthHeader } from './parts/AuthHeader';
import { AuthFooter } from './parts/AuthFooter';
import { AuthHeader } from './parts/AuthHeader';
import { LoginFormFields } from './type';

const StyledLogo = styled(Logo)`
margin: ${({ theme }) => theme.space.md};
Expand Down Expand Up @@ -81,21 +80,6 @@ const LoginPage = () => {
}
}, []);

const showGenericErrorToast = (title?: string) => {
addToast(
({ close }) => (
<Notification
onClose={close}
type="error"
message={`${title}${t('__TOAST_GENERIC_ERROR_MESSAGE')}`}
closeText={t('__TOAST_CLOSE_TEXT')}
isPrimary
/>
),
{ placement: 'top' }
);
};

const showInvalidCredentialsToast = () => {
addToast(
({ close }) => (
Expand Down Expand Up @@ -151,57 +135,16 @@ const LoginPage = () => {
// Login Cognito riuscito
setCta(`${t('__LOGIN_FORM_CTA_REDIRECT_STATE')}`);
document.location.href = from || '/';
return;
} catch (cognitoError: any) {
// Non è possibile gestire l'errore, se utente Cognito sbaglia password verrà fatto fallback al login legacy
showInvalidCredentialsToast();
setStatus({
message: t('__LOGIN_FORM_FAILED_INVALID'),
type: 'invalid',
});
setSubmitting(false);
// eslint-disable-next-line no-console
console.error('Cognito login error:', cognitoError);
}

// STEP 2: Fallback a WordPress legacy login (se utente non esiste su Cognito)
let nonce;
try {
nonce = await WPAPI.getNonce();
} catch (err: any) {
if (err?.status !== 200) {
showInvalidCredentialsToast();
setStatus({
message: t('__LOGIN_FORM_FAILED_INVALID'),
type: 'invalid',
});
setSubmitting(false);
return;
}
}
try {
await WPAPI.login({
username: values.email,
password: values.password,
security: nonce,
});

setCta(`${t('__LOGIN_FORM_CTA_REDIRECT_STATE')}`);
document.location.href = from || '/';
} catch (e: any) {
if (e?.status !== 200) {
showGenericErrorToast('Login: ');
setSubmitting(false);
return;
}
const { message } = e as Error;
const error = JSON.parse(message);

if (error.type === 'invalid') {
setStatus({
message: t('__LOGIN_FORM_FAILED_INVALID'),
type: 'invalid',
});
} else {
showGenericErrorToast();
}
}

setSubmitting(false);
};

return (
Expand Down
60 changes: 60 additions & 0 deletions src/pages/Media/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEffect } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { PageLoader } from 'src/common/components/PageLoader';
import {
useGetSignedMediaByIdQuery,
useGetUsersMeQuery,
} from 'src/features/api';
import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute';

const MediaPage = () => {
const { id } = useParams<{ id: string }>();
const { pathname } = useLocation();
const navigate = useNavigate();
const loginRoute = useLocalizeRoute('login');
const oopsRoute = useLocalizeRoute('media/oops');

const {
data,
error: signedError,
isLoading: isSignedLoading,
isFetching: isSignedFetching,
} = useGetSignedMediaByIdQuery({ id: id ?? '' }, { skip: !id });

const {
error: userError,
isLoading: isUserLoading,
isFetching: isUserFetching,
} = useGetUsersMeQuery();

useEffect(() => {
if (isSignedLoading || isSignedFetching) return;
if (data?.url) {
window.location.assign(data.url);
return;
}
if (!signedError) return;
if (isUserLoading || isUserFetching) return;
if (userError) {
navigate(loginRoute, { state: { from: pathname }, replace: true });
} else {
navigate(oopsRoute, { replace: true });
}
}, [
data,
signedError,
isSignedLoading,
isSignedFetching,
userError,
isUserLoading,
isUserFetching,
navigate,
loginRoute,
oopsRoute,
pathname,
]);

return <PageLoader />;
};

export default MediaPage;
Loading