WIP import_id #9
@ -77,6 +77,10 @@ const mimeTypesAccepted: { [key: string]: IMimeTypes } = {
|
||||
extension: "txt",
|
||||
size: 104857600, // 100MB
|
||||
},
|
||||
"application/json": {
|
||||
extension: "json",
|
||||
size: 104857600, // 100MB
|
||||
},
|
||||
};
|
||||
|
||||
type IDocumentFileBase = {
|
||||
|
@ -4,7 +4,7 @@ import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Ty
|
||||
//import Image from "next/image";
|
||||
import Form from "@Front/Components/DesignSystem/Form";
|
||||
import TextField from "@Front/Components/DesignSystem/Form/TextField";
|
||||
import Button from "@Front/Components/DesignSystem/Button";
|
||||
import Button, { EButtonVariant, EButtonstyletype } from "@Front/Components/DesignSystem/Button";
|
||||
//import franceConnectLogo from "../france-connect.svg";
|
||||
// import { useRouter } from "next/router";
|
||||
// import Customers from "@Front/Api/Auth/Id360/Customers/Customers";
|
||||
@ -20,10 +20,11 @@ import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm";
|
||||
type IProps = {
|
||||
onSubmit: (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => void;
|
||||
validationErrors: ValidationError[];
|
||||
onImportProfile?: () => void;
|
||||
};
|
||||
|
||||
export default function StepEmail(props: IProps) {
|
||||
const { onSubmit, validationErrors } = props;
|
||||
const { onSubmit, validationErrors, onImportProfile } = props;
|
||||
const [isErrorModalOpen, setIsErrorModalOpen] = useState(0);
|
||||
|
||||
/* const router = useRouter();
|
||||
@ -111,6 +112,20 @@ export default function StepEmail(props: IProps) {
|
||||
<Button type="submit">Se connecter</Button>
|
||||
</Form>
|
||||
</div>
|
||||
{onImportProfile && (
|
||||
<div className={classes["section"]}>
|
||||
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT} className={classes["section-title"]}>
|
||||
Options avancées :
|
||||
</Typography>
|
||||
<Button
|
||||
variant={EButtonVariant.SECONDARY}
|
||||
styletype={EButtonstyletype.TEXT}
|
||||
onClick={onImportProfile}
|
||||
>
|
||||
Importer un profil depuis un fichier
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Confirm
|
||||
isOpen={isErrorModalOpen === 1}
|
||||
|
@ -0,0 +1,72 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 530px;
|
||||
margin: auto;
|
||||
margin-top: 220px;
|
||||
|
||||
.title {
|
||||
text-align: left;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
font-family: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 16px;
|
||||
color: var(--color-neutral-700);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.import-section {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background-color: var(--color-error-50);
|
||||
border: 1px solid var(--color-error-200);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.profile-preview {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-neutral-50);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 8px;
|
||||
|
||||
.preview-title {
|
||||
margin-bottom: 12px;
|
||||
color: var(--color-neutral-800);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: flex-end;
|
||||
|
||||
.back-button {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
flex-direction: column;
|
||||
|
||||
.back-button,
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
116
src/front/Components/Layouts/Login/StepImportProfile/index.tsx
Normal file
116
src/front/Components/Layouts/Login/StepImportProfile/index.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import classes from "./classes.module.scss";
|
||||
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||
import DragAndDrop from "@Front/Components/DesignSystem/DragAndDrop";
|
||||
import { ValidationError } from "class-validator";
|
||||
|
||||
type IProps = {
|
||||
onSubmit: (profileData: any) => void;
|
||||
onBack: () => void;
|
||||
validationErrors: ValidationError[];
|
||||
};
|
||||
|
||||
export default function StepImportProfile(props: IProps) {
|
||||
const { onSubmit, onBack, validationErrors } = props;
|
||||
const [profileData, setProfileData] = useState<any>(null);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const validateProfileStructure = (data: any): boolean => {
|
||||
// Basic validation for profile structure
|
||||
return data &&
|
||||
typeof data === "object" &&
|
||||
data.version &&
|
||||
data.user_data &&
|
||||
typeof data.user_data === "object";
|
||||
};
|
||||
|
||||
const handleFileUpload = useCallback((files: File[]) => {
|
||||
const file = files[0];
|
||||
if (!file) return;
|
||||
|
||||
setError("");
|
||||
|
||||
// Read and parse JSON
|
||||
file.text()
|
||||
.then((text) => {
|
||||
const data = JSON.parse(text);
|
||||
|
||||
// Validate profile structure
|
||||
if (!validateProfileStructure(data)) {
|
||||
setError('Le fichier ne contient pas un profil valide.');
|
||||
return;
|
||||
}
|
||||
|
||||
setProfileData(data);
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(`Erreur lors de la lecture du fichier JSON: ${e}`);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (profileData) {
|
||||
onSubmit(profileData);
|
||||
}
|
||||
}, [profileData, onSubmit]);
|
||||
|
||||
// Get validation error for import
|
||||
const importError = validationErrors.find((error) => error.property === "import");
|
||||
|
||||
return (
|
||||
<div className={classes["root"]}>
|
||||
<Typography typo={ETypo.DISPLAY_LARGE}>
|
||||
<div className={classes["title"]}>Importer un profil</div>
|
||||
</Typography>
|
||||
|
||||
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["description"]}>
|
||||
Sélectionnez un fichier JSON contenant vos données de profil pour restaurer votre session.
|
||||
</Typography>
|
||||
|
||||
<div className={classes["import-section"]}>
|
||||
<DragAndDrop
|
||||
title="Importer un profil"
|
||||
description="Glissez-déposez votre fichier JSON ou cliquez pour parcourir"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(error || importError) && (
|
||||
<Typography typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_ERROR_500} className={classes["error"]}>
|
||||
{error || (importError?.constraints && Object.values(importError.constraints)[0])}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{profileData && (
|
||||
<div className={classes["profile-preview"]}>
|
||||
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} className={classes["preview-title"]}>
|
||||
Profil détecté :
|
||||
</Typography>
|
||||
<Typography typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_NEUTRAL_600}>
|
||||
Version : {profileData.version}
|
||||
</Typography>
|
||||
{profileData.exported_at && (
|
||||
<Typography typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_NEUTRAL_600}>
|
||||
Exporté le : {new Date(profileData.exported_at).toLocaleDateString('fr-FR')}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={classes["actions"]}>
|
||||
<Button variant={EButtonVariant.SECONDARY} onClick={onBack} className={classes["back-button"]}>
|
||||
Retour
|
||||
</Button>
|
||||
<Button
|
||||
variant={EButtonVariant.PRIMARY}
|
||||
onClick={handleSubmit}
|
||||
disabled={!profileData}
|
||||
className={classes["submit-button"]}
|
||||
>
|
||||
Importer le profil
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -21,6 +21,9 @@ import UserStore from "@Front/Stores/UserStore";
|
||||
|
||||
import AuthModal from "src/sdk/AuthModal";
|
||||
import CustomerService from "src/common/Api/LeCoffreApi/sdk/CustomerService";
|
||||
import StepImportProfile from "./StepImportProfile";
|
||||
import MessageBus from "src/sdk/MessageBus";
|
||||
import Iframe from "src/sdk/Iframe";
|
||||
|
||||
export enum LoginStep {
|
||||
EMAIL,
|
||||
@ -28,6 +31,7 @@ export enum LoginStep {
|
||||
PASSWORD,
|
||||
NEW_PASSWORD,
|
||||
PASSWORD_FORGOTTEN,
|
||||
IMPORT_PROFILE,
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
@ -43,6 +47,8 @@ export default function Login() {
|
||||
const [partialPhoneNumber, setPartialPhoneNumber] = useState<string>("");
|
||||
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]);
|
||||
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showIframeForImport, setShowIframeForImport] = useState(false);
|
||||
|
||||
// const openErrorModal = useCallback(() => {
|
||||
// setIsErrorModalOpen(true);
|
||||
@ -241,10 +247,52 @@ export default function Login() {
|
||||
}
|
||||
}, [email, totpCodeUid]);
|
||||
|
||||
const onImportProfileSubmit = useCallback(async (profileData: any) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setValidationErrors([]);
|
||||
|
||||
// Show iframe for import operation (but hidden from view)
|
||||
setShowIframeForImport(true);
|
||||
|
||||
// Initialize message listener for import operation
|
||||
MessageBus.getInstance().initMessageListener();
|
||||
|
||||
// Wait for the iframe to be ready
|
||||
await MessageBus.getInstance().isReady();
|
||||
|
||||
await MessageBus.getInstance().importBackup(profileData);
|
||||
|
||||
// Clean up message listener after import
|
||||
MessageBus.getInstance().destroyMessageListener();
|
||||
setShowIframeForImport(false);
|
||||
|
||||
router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path);
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "import",
|
||||
constraints: {
|
||||
[error.http_status || "500"]: error.message || "Erreur lors de l'import du profil",
|
||||
},
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setShowIframeForImport(false);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<DefaultDoubleSidePage title={"Login"} image={backgroundImage}>
|
||||
<div className={classes["root"]}>
|
||||
{step === LoginStep.EMAIL && <StepEmail onSubmit={onEmailFormSubmit} validationErrors={validationErrors} />}
|
||||
{step === LoginStep.EMAIL && (
|
||||
<StepEmail
|
||||
onSubmit={onEmailFormSubmit}
|
||||
validationErrors={validationErrors}
|
||||
onImportProfile={() => setStep(LoginStep.IMPORT_PROFILE)}
|
||||
/>
|
||||
)}
|
||||
{step === LoginStep.TOTP && (
|
||||
<StepTotp
|
||||
onSubmit={onSmsCodeSubmit}
|
||||
@ -281,7 +329,15 @@ export default function Login() {
|
||||
});
|
||||
}}
|
||||
/>}
|
||||
{step === LoginStep.IMPORT_PROFILE && (
|
||||
<StepImportProfile
|
||||
onSubmit={onImportProfileSubmit}
|
||||
onBack={() => setStep(LoginStep.EMAIL)}
|
||||
validationErrors={validationErrors}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showIframeForImport && <Iframe showIframe={false} />}
|
||||
{/* <Confirm
|
||||
isOpen={isErrorModalOpen}
|
||||
onClose={closeErrorModal}
|
||||
|
@ -105,12 +105,7 @@ export default function LoginCallBack() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function getUser() {
|
||||
UserStore.instance.disconnect();
|
||||
|
||||
// TODO: review
|
||||
// HACK: If start with http://local.lecoffreio.4nkweb:3000/authorized-client
|
||||
// Replace with http://localhost:3000/authorized-client
|
||||
UserStore.instance.disconnect().then(() => {
|
||||
if (window.location.href.startsWith('http://local.lecoffreio.4nkweb:3000/authorized-client')) {
|
||||
window.location.href = window.location.href.replace('http://local.lecoffreio.4nkweb:3000/authorized-client', 'http://localhost:3000/authorized-client');
|
||||
return;
|
||||
@ -119,22 +114,11 @@ export default function LoginCallBack() {
|
||||
const code = router.query["code"];
|
||||
if (code) {
|
||||
try {
|
||||
const idNotUser: any = await Auth.getInstance().getIdNotUser(code as string);
|
||||
Auth.getInstance().getIdNotUser(code as string).then((idNotUser: any) => {
|
||||
setIdNotUser(idNotUser);
|
||||
setIsAuthModalOpen(true);
|
||||
/*
|
||||
const token: any = null;
|
||||
if (!token) return router.push(Module.getInstance().get().modules.pages.Login.props.path);
|
||||
await UserStore.instance.connect(token.accessToken, token.refreshToken);
|
||||
const jwt = JwtService.getInstance().decodeJwt();
|
||||
if (!jwt) return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1");
|
||||
if (jwt.rules && !jwt.rules.includes("GET folders")) {
|
||||
return router.push(Module.getInstance().get().modules.pages.Subscription.pages.New.props.path);
|
||||
}
|
||||
setIsAuthModalOpen(true);
|
||||
//return router.push(Module.getInstance().get().modules.pages.Folder.props.path);
|
||||
*/
|
||||
return;
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.http_status === 401 && e.message === "Email not found") {
|
||||
return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=3");
|
||||
@ -145,24 +129,8 @@ export default function LoginCallBack() {
|
||||
return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1");
|
||||
}
|
||||
}
|
||||
/*
|
||||
const refreshToken = CookieService.getInstance().getCookie("leCoffreRefreshToken");
|
||||
if (!refreshToken) return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1");
|
||||
const isTokenRefreshed = await JwtService.getInstance().refreshToken(refreshToken);
|
||||
const jwt = JwtService.getInstance().decodeJwt();
|
||||
if (!jwt) return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1");
|
||||
if (!jwt.rules.includes("GET folders")) {
|
||||
return router.push(Module.getInstance().get().modules.pages.Subscription.pages.New.props.path);
|
||||
}
|
||||
if (isTokenRefreshed) {
|
||||
//setIsAuthModalOpen(true);
|
||||
//return router.push(Module.getInstance().get().modules.pages.Folder.props.path);
|
||||
return;
|
||||
}
|
||||
*/
|
||||
return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=2");
|
||||
}
|
||||
getUser();
|
||||
});
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
|
@ -1,5 +1,6 @@
|
||||
import backgroundImage from "@Assets/images/background_refonte.svg";
|
||||
import Users from "@Front/Api/LeCoffreApi/Notary/Users/Users";
|
||||
import Button, { EButtonVariant, EButtonSize, EButtonstyletype } from "@Front/Components/DesignSystem/Button";
|
||||
import Form from "@Front/Components/DesignSystem/Form";
|
||||
import TextField from "@Front/Components/DesignSystem/Form/TextField";
|
||||
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
@ -9,6 +10,7 @@ import User from "le-coffre-resources/dist/Notary";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import classes from "./classes.module.scss";
|
||||
import MessageBus from "src/sdk/MessageBus";
|
||||
|
||||
export default function MyAccount() {
|
||||
const [user, setUser] = React.useState<User | null>(null);
|
||||
@ -33,6 +35,42 @@ export default function MyAccount() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const downloadBackupFile = (backupFile: string, filename: string = 'backup.json') => {
|
||||
try {
|
||||
// Create a blob with the backup data
|
||||
const blob = new Blob([backupFile], { type: 'application/json' });
|
||||
|
||||
// Create a download link
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
|
||||
// Trigger the download
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading backup file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateRestorationFile = () => {
|
||||
MessageBus.getInstance().exportBackupFile().then((backupFile) => {
|
||||
// Generate filename with current date
|
||||
const now = new Date();
|
||||
const dateString = now.toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||
const filename = `backup_${dateString}.json`;
|
||||
|
||||
downloadBackupFile(backupFile, filename);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DefaultDoubleSidePage title={"Mon Compte"} image={backgroundImage} showHeader={true}>
|
||||
<div className={classes["root"]}>
|
||||
@ -104,6 +142,16 @@ export default function MyAccount() {
|
||||
canCopy
|
||||
/>
|
||||
</Form>
|
||||
|
||||
<Button
|
||||
variant={EButtonVariant.PRIMARY}
|
||||
size={EButtonSize.LG}
|
||||
styletype={EButtonstyletype.CONTAINED}
|
||||
fullwidth={true}
|
||||
onClick={handleCreateRestorationFile}
|
||||
>
|
||||
Créer un fichier de restauration
|
||||
</Button>
|
||||
</div>
|
||||
</DefaultDoubleSidePage>
|
||||
);
|
||||
|
@ -644,6 +644,70 @@ export default class MessageBus {
|
||||
});
|
||||
}
|
||||
|
||||
public exportBackupFile(): Promise<any> {
|
||||
return new Promise<any>((resolve: (backupFile: any) => void, reject: (error: string) => void) => {
|
||||
this.checkToken().then(() => {
|
||||
const messageId = `EXPORT_BACKUP_${uuidv4()}`;
|
||||
|
||||
const unsubscribe = EventBus.getInstance().on('BACKUP_RETRIEVED', (responseId: string, backupFile: string) => {
|
||||
if (responseId !== messageId) {
|
||||
return;
|
||||
}
|
||||
unsubscribe();
|
||||
resolve(backupFile);
|
||||
});
|
||||
|
||||
const unsubscribeError = EventBus.getInstance().on('ERROR_BACKUP_RETRIEVED', (responseId: string, error: string) => {
|
||||
if (responseId !== messageId) {
|
||||
return;
|
||||
}
|
||||
unsubscribeError();
|
||||
reject(error);
|
||||
});
|
||||
|
||||
const user = User.getInstance();
|
||||
const accessToken = user.getAccessToken()!;
|
||||
|
||||
this.sendMessage({
|
||||
type: 'EXPORT_BACKUP',
|
||||
accessToken,
|
||||
messageId
|
||||
});
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
public importBackup(backupFile: any): Promise<void> {
|
||||
return new Promise<void>((resolve: () => void, reject: (error: string) => void) => {
|
||||
const messageId = `IMPORT_BACKUP_${uuidv4()}`;
|
||||
|
||||
const unsubscribe = EventBus.getInstance().on('BACKUP_IMPORTED', (responseId: string) => {
|
||||
console.log('BACKUP_IMPORTED', responseId, messageId);
|
||||
if (responseId !== messageId) {
|
||||
return;
|
||||
}
|
||||
unsubscribe();
|
||||
resolve();
|
||||
});
|
||||
|
||||
const unsubscribeError = EventBus.getInstance().on('ERROR_BACKUP_IMPORTED', (responseId: string, error: string) => {
|
||||
if (responseId !== messageId) {
|
||||
return;
|
||||
}
|
||||
unsubscribeError();
|
||||
reject(error);
|
||||
});
|
||||
|
||||
console.log('IMPORT_BACKUP', backupFile);
|
||||
|
||||
this.sendMessage({
|
||||
type: 'IMPORT_BACKUP',
|
||||
backupFile: JSON.stringify(backupFile),
|
||||
messageId
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public verifyMerkleProof(merkleProof: string, documentHash: string): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve: (isValid: boolean) => void, reject: (error: string) => void) => {
|
||||
this.checkToken().then(() => {
|
||||
@ -863,6 +927,14 @@ export default class MessageBus {
|
||||
this.doHandleMessage(message.messageId, 'MERKLE_PROOF_VALIDATED', message, (message: any) => message.isValid);
|
||||
break;
|
||||
|
||||
case 'BACKUP_RETRIEVED': // EXPORT_BACKUP
|
||||
this.doHandleMessage(message.messageId, 'BACKUP_RETRIEVED', message, (message: any) => message.backupFile);
|
||||
break;
|
||||
|
||||
case 'BACKUP_IMPORTED': // IMPORT_BACKUP
|
||||
this.doHandleMessage(message.messageId, 'BACKUP_IMPORTED', message, () => { });
|
||||
break;
|
||||
|
||||
case 'ERROR':
|
||||
console.error('Error:', message);
|
||||
this.errors[message.messageId] = message.error;
|
||||
|
Loading…
x
Reference in New Issue
Block a user