add restore functionality
This commit is contained in:
16
README.md
16
README.md
@@ -23,6 +23,22 @@ Features:
|
|||||||
2. Group the character to accounts by clicking on the character name and setting the account name
|
2. Group the character to accounts by clicking on the character name and setting the account name
|
||||||
3. Make sure your extractors are running!
|
3. Make sure your extractors are running!
|
||||||
|
|
||||||
|
## Backing up or moving character list to another device
|
||||||
|
|
||||||
|
Because everything is stored in the browser there is no way to populate the list. To help with this EVE PI provides Backup and Restore functionality using basic JSON file.
|
||||||
|
|
||||||
|
To dowload your list:
|
||||||
|
|
||||||
|
1. Click Dowload button in the top button bar
|
||||||
|
2. Find your backup json file in your Downloads folder
|
||||||
|
|
||||||
|
To restore your list:
|
||||||
|
|
||||||
|
**Take note that restoring the list will overwrite any local list that you have!**
|
||||||
|
|
||||||
|
1. Click Restore button in the top button bar
|
||||||
|
2. Use the dialog to select the file you previously dowloaded to restore the list.
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
All eve sso information is stored in your browser and refresh token is encrypted with apps EVE SSO secret. Backend processes only the token exchange, refresh and revoke that need the EVE_SSO_SECRET. Everything else is handled in frontend.
|
All eve sso information is stored in your browser and refresh token is encrypted with apps EVE SSO secret. Backend processes only the token exchange, refresh and revoke that need the EVE_SSO_SECRET. Everything else is handled in frontend.
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { CharacterContext } from "@/app/context/Context";
|
import { CharacterContext, SessionContext } from "@/app/context/Context";
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ export const DowloadButton = () => {
|
|||||||
href={`data:text/json;charset=utf-8,${encodeURIComponent(
|
href={`data:text/json;charset=utf-8,${encodeURIComponent(
|
||||||
JSON.stringify(characters)
|
JSON.stringify(characters)
|
||||||
)}`}
|
)}`}
|
||||||
download="pi-avanto-tk-characters.json"
|
download={`eve-pi-characters.json`}
|
||||||
>
|
>
|
||||||
Backup
|
Backup
|
||||||
</Button>
|
</Button>
|
||||||
|
21
src/app/components/Backup/UploadButton.tsx
Normal file
21
src/app/components/Backup/UploadButton.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Box, Button } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { UploadDialog } from "./UploadDialog";
|
||||||
|
|
||||||
|
export const UploadButton = () => {
|
||||||
|
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
onClick={() => setUploadDialogOpen(true)}
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</Button>
|
||||||
|
<UploadDialog
|
||||||
|
open={uploadDialogOpen}
|
||||||
|
closeDialog={() => setUploadDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
72
src/app/components/Backup/UploadDialog.tsx
Normal file
72
src/app/components/Backup/UploadDialog.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import { DialogActions, DialogContent, Typography } from "@mui/material";
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { CharacterContext } from "@/app/context/Context";
|
||||||
|
import { AccessToken } from "@/types";
|
||||||
|
|
||||||
|
export const UploadDialog = ({
|
||||||
|
open,
|
||||||
|
closeDialog,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
closeDialog: () => void;
|
||||||
|
}) => {
|
||||||
|
const [file, setFile] = useState();
|
||||||
|
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
const { restoreCharacters } = useContext(CharacterContext);
|
||||||
|
|
||||||
|
const error = new Error("Invalid input");
|
||||||
|
|
||||||
|
const validate = (characters: AccessToken[]) => {
|
||||||
|
characters.forEach((c) => {
|
||||||
|
if (!c.access_token) throw error;
|
||||||
|
if (!c.expires_at) throw error;
|
||||||
|
if (!c.refresh_token) throw error;
|
||||||
|
if (!c.character) throw error;
|
||||||
|
if (!c.character.characterId) throw error;
|
||||||
|
if (!c.character.name) throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
return characters;
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (file) {
|
||||||
|
fileReader.onload = (event) => {
|
||||||
|
if (!event || event.target === null) return;
|
||||||
|
const jsonOutput = event.target.result?.toString();
|
||||||
|
if (jsonOutput) restoreCharacters(validate(JSON.parse(jsonOutput)));
|
||||||
|
closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileReader.readAsText(file);
|
||||||
|
}
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
|
const changeHandler = (event: any) => {
|
||||||
|
setFile(event.target.files[0]);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={closeDialog}>
|
||||||
|
<DialogTitle>Restore your character list</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography>
|
||||||
|
The list must be exported from the same EVE PI instance to work!
|
||||||
|
</Typography>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
accept=".json"
|
||||||
|
onChange={changeHandler}
|
||||||
|
style={{ paddingTop: "1rem" }}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={closeDialog}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
@@ -7,6 +7,7 @@ import { CharacterContext } from "../context/Context";
|
|||||||
import { DowloadButton } from "./Backup/DowloadButton";
|
import { DowloadButton } from "./Backup/DowloadButton";
|
||||||
import { DiscordButton } from "./Discord/DiscordButton";
|
import { DiscordButton } from "./Discord/DiscordButton";
|
||||||
import { GitHubButton } from "./Github/GitHubButton";
|
import { GitHubButton } from "./Github/GitHubButton";
|
||||||
|
import { UploadButton } from "./Backup/UploadButton";
|
||||||
|
|
||||||
interface Grouped {
|
interface Grouped {
|
||||||
[key: string]: AccessToken[];
|
[key: string]: AccessToken[];
|
||||||
@@ -28,6 +29,7 @@ export const MainGrid = ({ sessionReady }: { sessionReady: boolean }) => {
|
|||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
<LoginButton />
|
<LoginButton />
|
||||||
<DowloadButton />
|
<DowloadButton />
|
||||||
|
<UploadButton />
|
||||||
<DiscordButton />
|
<DiscordButton />
|
||||||
<GitHubButton />
|
<GitHubButton />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@@ -5,10 +5,12 @@ export const CharacterContext = createContext<{
|
|||||||
characters: AccessToken[];
|
characters: AccessToken[];
|
||||||
deleteCharacter: (character: AccessToken) => void;
|
deleteCharacter: (character: AccessToken) => void;
|
||||||
updateCharacter: (character: AccessToken, update: CharacterUpdate) => void;
|
updateCharacter: (character: AccessToken, update: CharacterUpdate) => void;
|
||||||
|
restoreCharacters: (characters: AccessToken[]) => void;
|
||||||
}>({
|
}>({
|
||||||
characters: [],
|
characters: [],
|
||||||
deleteCharacter: () => {},
|
deleteCharacter: () => {},
|
||||||
updateCharacter: () => {},
|
updateCharacter: () => {},
|
||||||
|
restoreCharacters: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SessionContext = createContext<{
|
export const SessionContext = createContext<{
|
||||||
|
@@ -73,6 +73,10 @@ const Home = () => {
|
|||||||
return characters;
|
return characters;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const restoreCharacters = (characters: AccessToken[]) => {
|
||||||
|
refreshSession(characters).then(saveCharacters).then(setCharacters);
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize EVE PI
|
// Initialize EVE PI
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("api/env")
|
fetch("api/env")
|
||||||
@@ -106,6 +110,7 @@ const Home = () => {
|
|||||||
characters,
|
characters,
|
||||||
deleteCharacter,
|
deleteCharacter,
|
||||||
updateCharacter,
|
updateCharacter,
|
||||||
|
restoreCharacters,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MainGrid sessionReady={sessionReady} />
|
<MainGrid sessionReady={sessionReady} />
|
||||||
|
Reference in New Issue
Block a user