@@ -1,16 +0,0 @@ | |||||
import csv | |||||
from tld import get_tld | |||||
tlds = {} | |||||
with open('top-1m.csv', newline='') as csvfile: | |||||
lignes = csv.reader(csvfile) | |||||
for ligne in lignes: | |||||
tld = get_tld(ligne[1], fix_protocol=True) | |||||
tlds[tld] = tlds.get(tld, 0) + 1 | |||||
sorted_tlds = [] | |||||
for w in sorted(tlds, key=tlds.get, reverse=True): | |||||
if tlds[w] > 1 and "xn--" not in w: | |||||
sorted_tlds.append(w) | |||||
print(sorted_tlds) |
@@ -1,24 +0,0 @@ | |||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |||||
# dependencies | |||||
/node_modules | |||||
/.pnp | |||||
.pnp.js | |||||
# testing | |||||
/coverage | |||||
# production | |||||
/build | |||||
/src/index.css | |||||
# misc | |||||
.DS_Store | |||||
.env.local | |||||
.env.development.local | |||||
.env.test.local | |||||
.env.production.local | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* |
@@ -1,69 +0,0 @@ | |||||
{ | |||||
"name": "lesspass-web-component", | |||||
"version": "0.1.0", | |||||
"private": true, | |||||
"dependencies": { | |||||
"@fortawesome/fontawesome-svg-core": "^1.2.36", | |||||
"@fortawesome/free-brands-svg-icons": "^5.15.4", | |||||
"@fortawesome/free-solid-svg-icons": "^5.15.4", | |||||
"@fortawesome/react-fontawesome": "^0.1.16", | |||||
"@fullhuman/postcss-purgecss": "^4.1.3", | |||||
"@reduxjs/toolkit": "^1.7.1", | |||||
"@testing-library/jest-dom": "^5.16.1", | |||||
"@testing-library/react": "^12.1.2", | |||||
"@testing-library/user-event": "^13.5.0", | |||||
"@types/axios": "^0.14.0", | |||||
"@types/i18next": "^13.0.0", | |||||
"@types/jest": "^27.4.0", | |||||
"@types/node": "^17.0.12", | |||||
"@types/react": "^17.0.38", | |||||
"@types/react-dom": "^17.0.11", | |||||
"@types/react-i18next": "^8.1.0", | |||||
"@types/react-redux": "^7.1.22", | |||||
"@types/react-router-dom": "^5.3.3", | |||||
"@types/redux-mock-store": "^1.0.3", | |||||
"@types/redux-persist": "^4.3.1", | |||||
"autoprefixer": "^10.4.2", | |||||
"axios": "^0.25.0", | |||||
"history": "^5.2.0", | |||||
"i18next": "^21.6.9", | |||||
"i18next-browser-languagedetector": "^6.1.3", | |||||
"jest-environment-jsdom-sixteen": "^2.0.0", | |||||
"lesspass": "^9.2.0", | |||||
"npm-run-all": "^4.1.5", | |||||
"postcss-cli": "^9.1.0", | |||||
"postcss-discard-comments": "^5.0.2", | |||||
"postcss-import": "^14.0.2", | |||||
"react": "^17.0.2", | |||||
"react-dom": "^17.0.2", | |||||
"react-i18next": "^11.15.3", | |||||
"react-redux": "^7.2.6", | |||||
"react-router-dom": "^6.2.1", | |||||
"react-scripts": "5.0.0", | |||||
"redux-mock-store": "^1.5.4", | |||||
"redux-persist": "^6.0.0", | |||||
"tailwindcss": "^3.0.16", | |||||
"ts-jest": "^27.1.3", | |||||
"typescript": "~4.5.5" | |||||
}, | |||||
"scripts": { | |||||
"start": "npm-run-all --parallel watch:css start:react", | |||||
"build": "npm-run-all build:css build:react", | |||||
"build:css": "postcss src/styles/tailwind.css --output src/index.css --env production", | |||||
"watch:css": "postcss src/styles/tailwind.css --output src/index.css --watch", | |||||
"start:react": "react-scripts start", | |||||
"build:react": "react-scripts build", | |||||
"test": "CI=true react-scripts test --env=jest-environment-jsdom-sixteen", | |||||
"test:watch": "react-scripts test --env=jest-environment-jsdom-sixteen", | |||||
"eject": "react-scripts eject", | |||||
"prettier": "prettier --write \"src/**/*.js\"" | |||||
}, | |||||
"eslintConfig": { | |||||
"extends": "react-app" | |||||
}, | |||||
"browserslist": [ | |||||
"> 1%", | |||||
"last 2 versions", | |||||
"not dead" | |||||
] | |||||
} |
@@ -1,25 +0,0 @@ | |||||
const purgecss = require("@fullhuman/postcss-purgecss")({ | |||||
content: [ | |||||
"./src/**/*.jsx", | |||||
"./src/**/*.tsx", | |||||
"./src/**/*.js", | |||||
"./src/**/*.ts", | |||||
"./public/index.html", | |||||
], | |||||
defaultExtractor: (content) => { | |||||
const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []; | |||||
const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || []; | |||||
return broadMatches.concat(innerMatches); | |||||
}, | |||||
}); | |||||
module.exports = { | |||||
plugins: [ | |||||
require("postcss-import"), | |||||
require("tailwindcss"), | |||||
require("autoprefixer"), | |||||
...(process.env.NODE_ENV === "production" | |||||
? [purgecss, require("postcss-discard-comments")({ removeAll: true })] | |||||
: []), | |||||
], | |||||
}; |
@@ -1,40 +0,0 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<meta charset="utf-8" /> | |||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |||||
<meta name="theme-color" content="#3366cc" /> | |||||
<meta name="description" content="LessPass stateless password manager" /> | |||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> | |||||
<!-- | |||||
manifest.json provides metadata used when your web app is installed on a | |||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ | |||||
--> | |||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> | |||||
<!-- | |||||
Notice the use of %PUBLIC_URL% in the tags above. | |||||
It will be replaced with the URL of the `public` folder during the build. | |||||
Only files inside the `public` folder can be referenced from the HTML. | |||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will | |||||
work correctly both with client-side routing and a non-root public URL. | |||||
Learn how to configure a non-root public URL by running `npm run build`. | |||||
--> | |||||
<title>LessPass</title> | |||||
</head> | |||||
<body> | |||||
<noscript>You need to enable JavaScript to run this app.</noscript> | |||||
<div id="root"></div> | |||||
<!-- | |||||
This HTML file is a template. | |||||
If you open it directly in the browser, you will see an empty page. | |||||
You can add webfonts, meta tags, or analytics to this file. | |||||
The build step will place the bundled scripts into the <body> tag. | |||||
To begin the development, run `npm start` or `yarn start`. | |||||
To create a production bundle, use `npm run build` or `yarn build`. | |||||
--> | |||||
</body> | |||||
</html> |
@@ -1,25 +0,0 @@ | |||||
{ | |||||
"short_name": "LessPass", | |||||
"name": "LessPass stateless password manager", | |||||
"icons": [ | |||||
{ | |||||
"src": "favicon.ico", | |||||
"sizes": "64x64 32x32 24x24 16x16", | |||||
"type": "image/x-icon" | |||||
}, | |||||
{ | |||||
"src": "logo192.png", | |||||
"type": "image/png", | |||||
"sizes": "192x192" | |||||
}, | |||||
{ | |||||
"src": "logo512.png", | |||||
"type": "image/png", | |||||
"sizes": "512x512" | |||||
} | |||||
], | |||||
"start_url": ".", | |||||
"display": "standalone", | |||||
"theme_color": "#3366cc", | |||||
"background_color": "#ffffff" | |||||
} |
@@ -1,3 +0,0 @@ | |||||
# https://www.robotstxt.org/robotstxt.html | |||||
User-agent: * | |||||
Disallow: / |
@@ -1,258 +0,0 @@ | |||||
import React from "react"; | |||||
import { Provider } from "react-redux"; | |||||
import { render, screen, waitFor, fireEvent } from "@testing-library/react"; | |||||
import { createMemoryHistory, MemoryHistory } from "history"; | |||||
import App from "./App"; | |||||
import { Router } from "react-router-dom"; | |||||
import { I18nextProvider } from "react-i18next"; | |||||
import { initI18n } from "./i18n"; | |||||
import reduxStore from "./store"; | |||||
import { fakeLocalStore } from "./services/localStore"; | |||||
export const InitApp = ({ | |||||
store, | |||||
history = createMemoryHistory(), | |||||
}: { | |||||
store: Store; | |||||
history?: MemoryHistory; | |||||
}) => { | |||||
return ( | |||||
<Provider store={reduxStore}> | |||||
<Router history={history}> | |||||
<I18nextProvider i18n={initI18n(store)}> | |||||
<App store={store} /> | |||||
</I18nextProvider> | |||||
</Router> | |||||
</Provider> | |||||
); | |||||
}; | |||||
beforeAll(() => { | |||||
jest.useFakeTimers(); | |||||
jest.runAllTimers(); | |||||
}); | |||||
afterAll(() => { | |||||
jest.useRealTimers(); | |||||
}); | |||||
test("at startup master-password-input should be visible", async () => { | |||||
render(<InitApp store={fakeLocalStore()} />); | |||||
const masterPasswordInput = screen.queryByTestId( | |||||
"master-password-input" | |||||
) as HTMLInputElement; | |||||
await waitFor(() => { | |||||
expect(masterPasswordInput).toBeInTheDocument(); | |||||
expect(masterPasswordInput.type).toBe("password"); | |||||
expect(masterPasswordInput.value).toBe(""); | |||||
}); | |||||
}); | |||||
test("at startup master-password-input should be focused", async () => { | |||||
render(<InitApp store={fakeLocalStore()} />); | |||||
const masterPasswordInput = screen.queryByTestId( | |||||
"master-password-input" | |||||
) as HTMLInputElement; | |||||
await waitFor(() => { | |||||
expect(masterPasswordInput).toBe(document.activeElement); | |||||
}); | |||||
}); | |||||
test("at startup unlock page is not present if master password is in store", () => { | |||||
render(<InitApp store={fakeLocalStore({ masterPassword: "password" })} />); | |||||
const passwordGeneratorForm = screen.queryByTestId("password-generator-form"); | |||||
expect(passwordGeneratorForm).toBeInTheDocument(); | |||||
}); | |||||
test("at startup save-master-password-checkbox should be visible", async () => { | |||||
render(<InitApp store={fakeLocalStore()} />); | |||||
const saveMasterPasswordCheckbox = screen.queryByTestId( | |||||
"save-master-password-checkbox" | |||||
) as HTMLInputElement; | |||||
await waitFor(() => { | |||||
expect(saveMasterPasswordCheckbox).toBeInTheDocument(); | |||||
expect(saveMasterPasswordCheckbox.type).toBe("checkbox"); | |||||
expect(saveMasterPasswordCheckbox.checked).toBe(true); | |||||
}); | |||||
}); | |||||
test("hit enter on unlock page save master password in store", async () => { | |||||
const store = fakeLocalStore(); | |||||
expect(store.getItem("masterPassword")).not.toBe("hit enter"); | |||||
render(<InitApp store={store} />); | |||||
const masterPasswordInput = screen.queryByTestId( | |||||
"master-password-input" | |||||
) as HTMLInputElement; | |||||
expect(masterPasswordInput).toBeInTheDocument(); | |||||
fireEvent.change(masterPasswordInput, { target: { value: "hit enter" } }); | |||||
fireEvent.submit(masterPasswordInput); | |||||
expect(store.getItem("masterPassword")).toBe("hit enter"); | |||||
await waitFor(() => { | |||||
expect(screen.queryByTestId("password-generator-form")).toBeInTheDocument(); | |||||
}); | |||||
}); | |||||
test("click save button and uncheck save-master-password-checkbox don't save it local storage", async () => { | |||||
const store = fakeLocalStore(); | |||||
expect(store.getItem("masterPassword")).toBeNull(); | |||||
render(<InitApp store={store} />); | |||||
const masterPasswordInput = screen.queryByTestId( | |||||
"master-password-input" | |||||
) as HTMLInputElement; | |||||
expect(masterPasswordInput).toBeInTheDocument(); | |||||
fireEvent.change(masterPasswordInput, { | |||||
target: { value: "master password" }, | |||||
}); | |||||
const saveMasterPasswordCheckbox = screen.queryByTestId( | |||||
"save-master-password-checkbox" | |||||
) as HTMLInputElement; | |||||
fireEvent.click(saveMasterPasswordCheckbox); | |||||
const submitButton = screen.queryByTestId("unlock") as HTMLButtonElement; | |||||
fireEvent.click(submitButton); | |||||
expect(store.getItem("masterPassword")).toBeNull(); | |||||
await waitFor(() => { | |||||
expect(screen.queryByTestId("password-generator-form")).toBeInTheDocument(); | |||||
}); | |||||
}); | |||||
test("lock button is present if master password in local storage and redirect to root url", async () => { | |||||
render(<InitApp store={fakeLocalStore({ masterPassword: "password" })} />); | |||||
const lockButton = screen.queryByTestId("lock-button") as HTMLButtonElement; | |||||
expect(lockButton).toBeInTheDocument(); | |||||
fireEvent.click(lockButton); | |||||
const masterPasswordInput = screen.queryByTestId( | |||||
"master-password-input" | |||||
) as HTMLInputElement; | |||||
await waitFor(() => { | |||||
expect(masterPasswordInput).toBeInTheDocument(); | |||||
}); | |||||
}); | |||||
test("click on unlock with save-master-password-checkbox false save value in localstorage", async () => { | |||||
const store = fakeLocalStore(); | |||||
render(<InitApp store={store} />); | |||||
const saveMasterPasswordCheckbox = screen.queryByTestId( | |||||
"save-master-password-checkbox" | |||||
) as HTMLInputElement; | |||||
fireEvent.click(saveMasterPasswordCheckbox); | |||||
await waitFor(() => { | |||||
expect(saveMasterPasswordCheckbox).toBeInTheDocument(); | |||||
expect(saveMasterPasswordCheckbox.type).toBe("checkbox"); | |||||
expect(saveMasterPasswordCheckbox.checked).toBe(false); | |||||
expect(store.getItem("settings")).toBeNull(); | |||||
}); | |||||
const submitButton = screen.queryByTestId("unlock") as HTMLButtonElement; | |||||
fireEvent.click(submitButton); | |||||
await waitFor(() => { | |||||
expect(store.getItem("settings")).toMatchObject({ | |||||
saveMasterPassword: false, | |||||
}); | |||||
}); | |||||
}); | |||||
test("save-master-password-checkbox is unchecked if user already choose not to save master password", async () => { | |||||
render( | |||||
<InitApp | |||||
store={fakeLocalStore({ settings: { saveMasterPassword: false } })} | |||||
/> | |||||
); | |||||
const saveMasterPasswordCheckbox = screen.queryByTestId( | |||||
"save-master-password-checkbox" | |||||
) as HTMLInputElement; | |||||
await waitFor(() => { | |||||
expect(saveMasterPasswordCheckbox).toBeInTheDocument(); | |||||
expect(saveMasterPasswordCheckbox.type).toBe("checkbox"); | |||||
expect(saveMasterPasswordCheckbox.checked).toBe(false); | |||||
}); | |||||
}); | |||||
test("click on unlock with save-master-password-checkbox false and remove master password from localstorage", async () => { | |||||
const store = fakeLocalStore({ masterPassword: "password" }); | |||||
render(<InitApp store={store} />); | |||||
const lockButton = screen.queryByTestId("lock-button") as HTMLButtonElement; | |||||
fireEvent.click(lockButton); | |||||
const saveMasterPasswordCheckbox = screen.queryByTestId( | |||||
"save-master-password-checkbox" | |||||
) as HTMLInputElement; | |||||
fireEvent.click(saveMasterPasswordCheckbox); | |||||
const submitButton = screen.queryByTestId("unlock") as HTMLButtonElement; | |||||
fireEvent.click(submitButton); | |||||
await waitFor(() => { | |||||
expect(store.getItem("masterPassword")).toBeNull(); | |||||
}); | |||||
}); | |||||
test("routing pages unauth", async () => { | |||||
const history = createMemoryHistory(); | |||||
render( | |||||
<InitApp | |||||
history={history} | |||||
store={fakeLocalStore({ masterPassword: "password" })} | |||||
/> | |||||
); | |||||
expect(history.location.pathname).toBe("/"); | |||||
fireEvent.click(screen.queryByText(/lesspass/i) as HTMLLinkElement); | |||||
expect(history.location.pathname).toBe("/"); | |||||
fireEvent.click(screen.queryByTestId("settings-link") as HTMLLinkElement); | |||||
expect(history.location.pathname).toBe("/settings"); | |||||
expect(screen.queryByText(/passwords/i)).toBeNull(); | |||||
fireEvent.click(screen.queryByTestId("sign-in-link") as HTMLLinkElement); | |||||
expect(history.location.pathname).toBe("/signIn"); | |||||
fireEvent.click(screen.queryByTestId("register-link") as HTMLLinkElement); | |||||
expect(history.location.pathname).toBe("/register"); | |||||
expect(screen.queryByTestId("my-account-link")).toBeNull(); | |||||
}); | |||||
test("routing pages auth", async () => { | |||||
const history = createMemoryHistory(); | |||||
render( | |||||
<InitApp | |||||
history={history} | |||||
store={fakeLocalStore({ | |||||
masterPassword: "password", | |||||
access_token: "access_token", | |||||
})} | |||||
/> | |||||
); | |||||
expect(history.location.pathname).toBe("/"); | |||||
fireEvent.click(screen.queryByText(/lesspass/i) as HTMLLinkElement); | |||||
expect(history.location.pathname).toBe("/"); | |||||
fireEvent.click(screen.queryByTestId("settings-link") as HTMLLinkElement); | |||||
expect(history.location.pathname).toBe("/settings"); | |||||
await waitFor(() => { | |||||
expect( | |||||
screen.queryByText(/passwords/i) as HTMLLinkElement | |||||
).toBeInTheDocument(); | |||||
}); | |||||
fireEvent.click(screen.queryByText(/passwords/i) as HTMLLinkElement); | |||||
expect(history.location.pathname).toBe("/passwords"); | |||||
expect(screen.queryByTestId("sign-in-link")).toBeNull(); | |||||
expect(screen.queryByTestId("register-link")).toBeNull(); | |||||
fireEvent.click(screen.queryByTestId("my-account-link") as HTMLLinkElement); | |||||
expect(history.location.pathname).toBe("/my_account"); | |||||
}); | |||||
test("test i18n fr", async () => { | |||||
render(<InitApp store={fakeLocalStore({ masterPassword: "password" })} />); | |||||
expect(screen.queryByText(/se connecter/i)).toBeNull(); | |||||
fireEvent.click(screen.queryByTestId("settings-link") as HTMLLinkElement); | |||||
fireEvent.click(screen.queryByLabelText(/fr/i) as HTMLInputElement); | |||||
expect(screen.queryByText(/se connecter/i)).toBeInTheDocument(); | |||||
fireEvent.click(screen.queryByLabelText(/en/i) as HTMLInputElement); | |||||
expect(screen.queryByText(/sign in/i)).toBeInTheDocument(); | |||||
}); | |||||
test("password generator page can't be reach if master password is not unlocked", async () => { | |||||
const history = createMemoryHistory(); | |||||
render(<InitApp history={history} store={fakeLocalStore()} />); | |||||
const masterPasswordInput = screen.queryByTestId( | |||||
"master-password-input" | |||||
) as HTMLInputElement; | |||||
fireEvent.change(masterPasswordInput, { | |||||
target: { value: "p" }, | |||||
}); | |||||
fireEvent.click(screen.queryByText(/lesspass/i) as HTMLLinkElement); | |||||
await waitFor(() => { | |||||
expect(history.location.pathname).toBe("/unlock"); | |||||
}); | |||||
}); |
@@ -1,120 +0,0 @@ | |||||
import React, { useState, useEffect } from "react"; | |||||
import "./icons"; | |||||
import { Switch, Route, useHistory, Redirect } from "react-router-dom"; | |||||
import PasswordGeneratorPage from "./passwordGenerator/PasswordGeneratorPage"; | |||||
import UnlockPage from "./unlock/UnlockPage"; | |||||
import Nav from "./components/Nav"; | |||||
import RegisterPage from "./auth/RegisterPage"; | |||||
import SignInPage from "./auth/SignInPage"; | |||||
import SettingsPage from "./settings/SettingsPage"; | |||||
import defaultSettings from "./settings/defaultSettings"; | |||||
import PasswordsPage from "./passwords/PasswordsPage"; | |||||
import { useDispatch } from "react-redux"; | |||||
import { signInSuccess, logout } from "./auth/authSlice"; | |||||
import { | |||||
MASTER_PASSWORD_KEY, | |||||
SETTINGS_KEY, | |||||
ACCESS_TOKEN, | |||||
} from "./services/localStore"; | |||||
function useLocalStore<T>( | |||||
key: string, | |||||
initialValue: T, | |||||
store: Store | |||||
): [T, (t: T) => void] { | |||||
const [storedValue, setStoredValue] = useState<T>( | |||||
(): T => { | |||||
try { | |||||
const value = (store.getItem(key) as unknown) as T; | |||||
return value || initialValue; | |||||
} catch (error) { | |||||
return initialValue; | |||||
} | |||||
} | |||||
); | |||||
const setValue = (value: T) => { | |||||
try { | |||||
setStoredValue(value); | |||||
store.setItem(key, (value as unknown) as Serializable); | |||||
} catch (error) {} | |||||
}; | |||||
return [storedValue, setValue]; | |||||
} | |||||
const App = ({ store }: { store: Store }) => { | |||||
const dispatch = useDispatch(); | |||||
const history = useHistory(); | |||||
const [masterPassword, setMasterPassword] = useState<MasterPassword>( | |||||
() => (store.getItem(MASTER_PASSWORD_KEY) as string) || "" | |||||
); | |||||
const [settings, setSettings] = useLocalStore<Settings>( | |||||
SETTINGS_KEY, | |||||
defaultSettings, | |||||
store | |||||
); | |||||
useEffect(() => { | |||||
const access_token = store.getItem(ACCESS_TOKEN); | |||||
if (access_token) { | |||||
dispatch( | |||||
signInSuccess({ | |||||
access: access_token, | |||||
} as SignInResponsePayload) | |||||
); | |||||
} else { | |||||
dispatch(logout()); | |||||
} | |||||
}, [dispatch, store]); | |||||
return ( | |||||
<div> | |||||
<Nav /> | |||||
<Switch> | |||||
<Route exact path="/unlock"> | |||||
<UnlockPage | |||||
settings={settings} | |||||
unlock={(newMasterPassword, saveMasterPassword) => { | |||||
if (saveMasterPassword) { | |||||
store.setItem(MASTER_PASSWORD_KEY, newMasterPassword); | |||||
} else { | |||||
store.removeItem(MASTER_PASSWORD_KEY); | |||||
} | |||||
setMasterPassword(newMasterPassword); | |||||
setSettings({ ...settings, saveMasterPassword }); | |||||
history.push("/"); | |||||
}} | |||||
/> | |||||
</Route> | |||||
<Route exact path="/"> | |||||
{masterPassword ? ( | |||||
<PasswordGeneratorPage | |||||
masterPassword={masterPassword} | |||||
setMasterPassword={setMasterPassword} | |||||
/> | |||||
) : ( | |||||
<Redirect to="/unlock" /> | |||||
)} | |||||
</Route> | |||||
<Route exact path="/register"> | |||||
<RegisterPage /> | |||||
</Route> | |||||
<Route exact path="/signIn"> | |||||
<SignInPage /> | |||||
</Route> | |||||
<Route exact path="/settings"> | |||||
<SettingsPage | |||||
settings={settings} | |||||
setSettings={setSettings} | |||||
/> | |||||
</Route> | |||||
<Route exact path="/passwords"> | |||||
<PasswordsPage /> | |||||
</Route> | |||||
</Switch> | |||||
</div> | |||||
); | |||||
}; | |||||
export default App; |
@@ -1,55 +0,0 @@ | |||||
import React from "react"; | |||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; | |||||
import RegisterForm from "./RegisterForm"; | |||||
it("Registerform on submit", async () => { | |||||
const mockOnSubmit = jest.fn(); | |||||
render(<RegisterForm onSubmit={mockOnSubmit} />); | |||||
const signInForm = screen.getByTestId("register-form"); | |||||
expect(signInForm).not.toBe(null); | |||||
const email = screen.getByTestId("email-input"); | |||||
fireEvent.change(email, { | |||||
target: { | |||||
value: "contact@example.org", | |||||
}, | |||||
}); | |||||
const password = screen.getByTestId("password-input"); | |||||
fireEvent.change(password, { | |||||
target: { | |||||
value: "password", | |||||
}, | |||||
}); | |||||
fireEvent.submit(signInForm); | |||||
await waitFor(() => { | |||||
expect(mockOnSubmit.mock.calls.length).toBe(1); | |||||
expect(mockOnSubmit.mock.calls[0][0]).toEqual({ | |||||
email: "contact@example.org", | |||||
password: "password", | |||||
}); | |||||
}); | |||||
}); | |||||
it("Registerform on submit is not triggered if use doesn't accept Terms of service", async () => { | |||||
const mockOnSubmit = jest.fn(); | |||||
render(<RegisterForm onSubmit={mockOnSubmit} />); | |||||
const signInForm = screen.getByTestId("register-form"); | |||||
expect(signInForm).not.toBe(null); | |||||
const email = screen.getByTestId("email-input"); | |||||
fireEvent.change(email, { | |||||
target: { | |||||
value: "contact@example.org", | |||||
}, | |||||
}); | |||||
const password = screen.getByTestId("password-input"); | |||||
fireEvent.change(password, { | |||||
target: { | |||||
value: "password", | |||||
}, | |||||
}); | |||||
const tou = screen.getByTestId("term-of-use-checkbox"); | |||||
fireEvent.click(tou); | |||||
fireEvent.submit(signInForm); | |||||
await waitFor(() => { | |||||
expect(mockOnSubmit.mock.calls.length).toBe(0); | |||||
}); | |||||
}); |
@@ -1,70 +0,0 @@ | |||||
import React, { useState } from "react"; | |||||
type Credentials = { | |||||
email: string; | |||||
password: string; | |||||
}; | |||||
type RegisterFormProps = { | |||||
id?: string; | |||||
onSubmit: (credentials: Credentials) => void; | |||||
}; | |||||
const RegisterForm = ({ | |||||
id = "register-form", | |||||
onSubmit, | |||||
}: RegisterFormProps) => { | |||||
const [email, setEmail] = useState<string>(""); | |||||
const [password, setPassword] = useState<string>(""); | |||||
const [termOfUse, setTermOfUse] = useState(true); | |||||
return ( | |||||
<form | |||||
id={id} | |||||
data-testid={id} | |||||
onSubmit={(event) => { | |||||
event.preventDefault(); | |||||
const credential = { | |||||
email, | |||||
password, | |||||
}; | |||||
if (termOfUse) { | |||||
onSubmit(credential); | |||||
} | |||||
}} | |||||
> | |||||
<label htmlFor="email-input">Email</label> | |||||
<input | |||||
type="email" | |||||
id="email-input" | |||||
data-testid="email-input" | |||||
name="email" | |||||
value={email} | |||||
onChange={(event) => setEmail(event.target.value)} | |||||
/> | |||||
<label htmlFor="password-input">Password</label> | |||||
<input | |||||
type="password" | |||||
id="password-input" | |||||
data-testid="password-input" | |||||
name="password" | |||||
value={password} | |||||
onChange={(event) => setPassword(event.target.value)} | |||||
/> | |||||
<input | |||||
type="checkbox" | |||||
id="term-of-use-checkbox" | |||||
data-testid="term-of-use-checkbox" | |||||
name="term-of-use" | |||||
checked={termOfUse} | |||||
onChange={(event) => setTermOfUse(event.target.checked)} | |||||
/> | |||||
<label htmlFor="term-of-use-checkbox">accept terms of use</label> | |||||
<button type="submit" id="register-button" data-testid="register-button"> | |||||
Create an account | |||||
</button> | |||||
</form> | |||||
); | |||||
}; | |||||
export default RegisterForm; |
@@ -1,20 +0,0 @@ | |||||
import React from "react"; | |||||
import { Link } from "react-router-dom"; | |||||
import SignInForm from "./SignInForm"; | |||||
import { useTranslation } from "react-i18next"; | |||||
const RegisterPage = () => { | |||||
const { t } = useTranslation(); | |||||
return ( | |||||
<div> | |||||
<SignInForm | |||||
id="sign-in-form" | |||||
onSubmit={(credential) => console.log(credential)} | |||||
/> | |||||
{t("auth.alreadyhaveanaccount")} | |||||
<Link to="/signIn">sign in</Link> | |||||
</div> | |||||
); | |||||
}; | |||||
export default RegisterPage; |
@@ -1,30 +0,0 @@ | |||||
import React from "react"; | |||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; | |||||
import SignInForm from "./SignInForm"; | |||||
it("SignInform on submit", async () => { | |||||
const mockOnSubmit = jest.fn(); | |||||
render(<SignInForm onSubmit={mockOnSubmit} />); | |||||
const signInForm = screen.getByTestId("sign-in-form"); | |||||
expect(signInForm).not.toBe(null); | |||||
const email = screen.getByTestId("email-input"); | |||||
fireEvent.change(email, { | |||||
target: { | |||||
value: "contact@example.org", | |||||
}, | |||||
}); | |||||
const password = screen.getByTestId("password-input"); | |||||
fireEvent.change(password, { | |||||
target: { | |||||
value: "password", | |||||
}, | |||||
}); | |||||
fireEvent.submit(signInForm); | |||||
await waitFor(() => { | |||||
expect(mockOnSubmit.mock.calls.length).toBe(1); | |||||
expect(mockOnSubmit.mock.calls[0][0]).toEqual({ | |||||
email: "contact@example.org", | |||||
password: "password", | |||||
}); | |||||
}); | |||||
}); |
@@ -1,54 +0,0 @@ | |||||
import React, { useState } from "react"; | |||||
type Credentials = { | |||||
email: string; | |||||
password: string; | |||||
}; | |||||
type SignInFormProps = { | |||||
id?: string; | |||||
onSubmit: (credentials: Credentials) => void; | |||||
}; | |||||
const SignInForm = ({ id = "sign-in-form", onSubmit }: SignInFormProps) => { | |||||
const [email, setEmail] = useState<string>(""); | |||||
const [password, setPassword] = useState<string>(""); | |||||
return ( | |||||
<form | |||||
id={id} | |||||
data-testid={id} | |||||
onSubmit={(event) => { | |||||
event.preventDefault(); | |||||
const credential = { | |||||
email, | |||||
password, | |||||
}; | |||||
onSubmit(credential); | |||||
}} | |||||
> | |||||
<label htmlFor="email-input">Email</label> | |||||
<input | |||||
type="email" | |||||
id="email-input" | |||||
data-testid="email-input" | |||||
name="email" | |||||
value={email} | |||||
onChange={(event) => setEmail(event.target.value)} | |||||
/> | |||||
<label htmlFor="password-input">Password</label> | |||||
<input | |||||
type="password" | |||||
id="password-input" | |||||
data-testid="password-input" | |||||
name="password" | |||||
value={password} | |||||
onChange={(event) => setPassword(event.target.value)} | |||||
/> | |||||
<button type="submit" id="sign-in-button" data-testid="sign-in-button"> | |||||
Sign In | |||||
</button> | |||||
</form> | |||||
); | |||||
}; | |||||
export default SignInForm; |
@@ -1,22 +0,0 @@ | |||||
import React from "react"; | |||||
import { Link } from "react-router-dom"; | |||||
import SignInForm from "./SignInForm"; | |||||
import { useTranslation } from "react-i18next"; | |||||
const SignInPage = () => { | |||||
const { t } = useTranslation(); | |||||
return ( | |||||
<div> | |||||
<SignInForm | |||||
id="sign-in-form" | |||||
onSubmit={(credential) => console.log(credential)} | |||||
/> | |||||
{t("auth.donthaveanaccount")} | |||||
<Link data-testid="register-link" to="/register"> | |||||
register | |||||
</Link> | |||||
</div> | |||||
); | |||||
}; | |||||
export default SignInPage; |
@@ -1,56 +0,0 @@ | |||||
import configureMockStore from "redux-mock-store"; | |||||
import { mocked } from "ts-jest/utils"; | |||||
import thunk from "redux-thunk"; | |||||
import * as api from "../services/api"; | |||||
import { | |||||
AuthState, | |||||
initialState, | |||||
signIn, | |||||
signInStart, | |||||
signInSuccess, | |||||
signInFailure, | |||||
} from "./authSlice"; | |||||
import { AppDispatch } from "../store"; | |||||
jest.mock("../services/api"); | |||||
const mockedApi = mocked(api, true); | |||||
const mockedStore = configureMockStore<AuthState, AppDispatch>([thunk]); | |||||
test("creates both signInStart and signInSuccess when signIn succeeds", async () => { | |||||
const requestPayload = { | |||||
email: "contact@example.org", | |||||
password: "password", | |||||
}; | |||||
const responsePayload = { | |||||
access: "access_token", | |||||
refresh: "refresh_token", | |||||
}; | |||||
const store = mockedStore(initialState); | |||||
mockedApi.signIn.mockResolvedValueOnce(responsePayload); | |||||
await store.dispatch(signIn(requestPayload)); | |||||
const expectedActions = [signInStart(), signInSuccess(responsePayload)]; | |||||
expect(store.getActions()).toEqual(expectedActions); | |||||
}); | |||||
test("creates both signInStart and signInFailure when signIn fails", async () => { | |||||
const requestPayload = { | |||||
email: "contact@example.org", | |||||
password: "wrong password", | |||||
}; | |||||
const responseError = new Error("Invalid credentials"); | |||||
const store = mockedStore(initialState); | |||||
mockedApi.signIn.mockRejectedValueOnce(responseError); | |||||
await store.dispatch(signIn(requestPayload)); | |||||
const expectedActions = [ | |||||
signInStart(), | |||||
signInFailure({ error: responseError }), | |||||
]; | |||||
expect(store.getActions()).toEqual(expectedActions); | |||||
}); |
@@ -1,63 +0,0 @@ | |||||
import reducer, { | |||||
initialState, | |||||
signInStart, | |||||
signInSuccess, | |||||
signInFailure, | |||||
logout, | |||||
selectIsLoading, | |||||
selectError, | |||||
selectIsAuthenticated, | |||||
selectToken, | |||||
} from "./authSlice"; | |||||
import { PayloadAction } from "@reduxjs/toolkit"; | |||||
import { RootState } from "../store"; | |||||
test("should return the initial state", () => { | |||||
const nextState = initialState; | |||||
const result = reducer(undefined, {} as PayloadAction); | |||||
expect(result).toEqual(nextState); | |||||
}); | |||||
test("should properly set loading and error state when signInStart", () => { | |||||
const nextState = reducer(initialState, signInStart()); | |||||
const rootState = { auth: nextState } as RootState; | |||||
expect(selectIsAuthenticated(rootState)).toEqual(false); | |||||
expect(selectIsLoading(rootState)).toEqual(true); | |||||
expect(selectError(rootState)).toEqual(null); | |||||
}); | |||||
it("should properly set loading, error and user information when signInSuccess", () => { | |||||
const payload = { access: "access_token" }; | |||||
const nextState = reducer(initialState, signInSuccess(payload)); | |||||
const rootState = { auth: nextState } as RootState; | |||||
expect(selectIsAuthenticated(rootState)).toEqual(true); | |||||
expect(selectToken(rootState)).toEqual(payload.access); | |||||
expect(selectIsLoading(rootState)).toEqual(false); | |||||
expect(selectError(rootState)).toEqual(null); | |||||
}); | |||||
it("should properly set loading, error and remove user information when signInFailure", () => { | |||||
const error = new Error("Incorrect password"); | |||||
const nextState = reducer( | |||||
initialState, | |||||
signInFailure({ error: error.message }) | |||||
); | |||||
const rootState = { auth: nextState } as RootState; | |||||
expect(selectIsAuthenticated(rootState)).toEqual(false); | |||||
expect(selectToken(rootState)).toEqual(null); | |||||
expect(selectIsLoading(rootState)).toEqual(false); | |||||
expect(selectError(rootState)).toEqual(error.message); | |||||
}); | |||||
it("should properly set loading, error and remove user information when token is null", () => { | |||||
const payload = { access: "access_token" }; | |||||
const nextState = reducer( | |||||
reducer(initialState, signInSuccess(payload)), | |||||
logout() | |||||
); | |||||
const rootState = { auth: nextState } as RootState; | |||||
expect(selectIsAuthenticated(rootState)).toEqual(false); | |||||
expect(selectToken(rootState)).toEqual(null); | |||||
expect(selectIsLoading(rootState)).toEqual(false); | |||||
expect(selectError(rootState)).toEqual(null); | |||||
}); |
@@ -1,72 +0,0 @@ | |||||
import { createSlice, createSelector, PayloadAction } from "@reduxjs/toolkit"; | |||||
import * as api from "../services/api"; | |||||
import { RootState, AppDispatch } from "../store"; | |||||
export type AuthState = { | |||||
isLoading: boolean; | |||||
token: AccessToken | null; | |||||
error: string | null; | |||||
}; | |||||
export const initialState: AuthState = { | |||||
isLoading: false, | |||||
token: null, | |||||
error: null, | |||||
}; | |||||
const authSlice = createSlice({ | |||||
name: "auth", | |||||
initialState, | |||||
reducers: { | |||||
signInStart(state) { | |||||
state.isLoading = true; | |||||
state.error = null; | |||||
}, | |||||
signInSuccess(state, action: PayloadAction<SignInResponsePayload>) { | |||||
const { access } = action.payload; | |||||
state.token = access; | |||||
state.isLoading = false; | |||||
state.error = null; | |||||
}, | |||||
signInFailure(state, action) { | |||||
const { error } = action.payload; | |||||
state.isLoading = false; | |||||
state.token = null; | |||||
state.error = error; | |||||
}, | |||||
logout(state) { | |||||
state.isLoading = false; | |||||
state.token = null; | |||||
state.error = null; | |||||
}, | |||||
}, | |||||
}); | |||||
export const { | |||||
signInStart, | |||||
signInSuccess, | |||||
signInFailure, | |||||
logout, | |||||
} = authSlice.actions; | |||||
export default authSlice.reducer; | |||||
export const selectToken = (state: RootState) => state.auth.token; | |||||
export const selectError = (state: RootState) => state.auth.error; | |||||
export const selectIsLoading = (state: RootState) => state.auth.isLoading; | |||||
export const selectIsAuthenticated = createSelector( | |||||
[selectToken], | |||||
(token) => token !== null | |||||
); | |||||
export const signIn = ({ email, password }: SignInRequestPayload) => async ( | |||||
dispatch: AppDispatch | |||||
) => { | |||||
try { | |||||
dispatch(signInStart()); | |||||
const tokens = await api.signIn({ email, password }); | |||||
dispatch(signInSuccess(tokens)); | |||||
} catch (error) { | |||||
dispatch(signInFailure({ error })); | |||||
} | |||||
}; |
@@ -1,49 +0,0 @@ | |||||
import React from "react"; | |||||
import { Link } from "react-router-dom"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { useSelector } from "react-redux"; | |||||
import { selectIsAuthenticated } from "../auth/authSlice"; | |||||
const Nav = () => { | |||||
const { t } = useTranslation(); | |||||
const isAuthenticated = useSelector(selectIsAuthenticated); | |||||
if (isAuthenticated) { | |||||
return ( | |||||
<nav> | |||||
<ul> | |||||
<li> | |||||
<Link to="/">LessPass</Link> | |||||
</li> | |||||
<li> | |||||
<Link data-testid="password-link" to="/passwords">{t("nav.passwords")}</Link> | |||||
</li> | |||||
<li> | |||||
<Link data-testid="settings-link" to="/settings"> | |||||
{t("nav.settings")} | |||||
</Link> | |||||
</li> | |||||
<li> | |||||
<Link data-testid="my-account-link" to="/my_account">{t("nav.myaccount")}</Link> | |||||
</li> | |||||
</ul> | |||||
</nav> | |||||
); | |||||
} | |||||
return ( | |||||
<nav> | |||||
<ul> | |||||
<li> | |||||
<Link to="/">LessPass</Link> | |||||
</li> | |||||
<li> | |||||
<Link data-testid="settings-link" to="/settings">{t("nav.settings")}</Link> | |||||
</li> | |||||
<li> | |||||
<Link data-testid="sign-in-link" to="/signIn">{t("nav.signin")}</Link> | |||||
</li> | |||||
</ul> | |||||
</nav> | |||||
); | |||||
}; | |||||
export default Nav; |
@@ -1,123 +0,0 @@ | |||||
type Serializable = object | string | boolean; | |||||
type Store = { | |||||
getItem(key: string): Serializable | null; | |||||
setItem(key: string, value: Serializable): void; | |||||
removeItem(key: string): void; | |||||
}; | |||||
type MasterPassword = string; | |||||
type Settings = { | |||||
saveMasterPassword: boolean; | |||||
useMasterPasswordForAuth: boolean; | |||||
language: string | null; | |||||
}; | |||||
type PasswordProfile = { | |||||
site: string; | |||||
login: string; | |||||
lowercase: boolean; | |||||
uppercase: boolean; | |||||
digits: boolean; | |||||
symbols: boolean; | |||||
length: number; | |||||
counter: number; | |||||
}; | |||||
type LegacyPasswordProfile = { | |||||
id:string; | |||||
site: string; | |||||
login: string; | |||||
lowercase: boolean; | |||||
uppercase: boolean; | |||||
numbers: boolean; | |||||
symbols: boolean; | |||||
length: number; | |||||
counter: number; | |||||
}; | |||||
type FingerprintColor = | |||||
| "#000000" | |||||
| "#074750" | |||||
| "#009191" | |||||
| "#FF6CB6" | |||||
| "#FFB5DA" | |||||
| "#490092" | |||||
| "#006CDB" | |||||
| "#B66DFF" | |||||
| "#6DB5FE" | |||||
| "#B5DAFE" | |||||
| "#920000" | |||||
| "#924900" | |||||
| "#DB6D00" | |||||
| "#24FE23"; | |||||
type FingerprintIcon = | |||||
| "fa-hashtag" | |||||
| "fa-heart" | |||||
| "fa-hotel" | |||||
| "fa-university" | |||||
| "fa-plug" | |||||
| "fa-ambulance" | |||||
| "fa-bus" | |||||
| "fa-car" | |||||
| "fa-plane" | |||||
| "fa-rocket" | |||||
| "fa-ship" | |||||
| "fa-subway" | |||||
| "fa-truck" | |||||
| "fa-jpy" | |||||
| "fa-eur" | |||||
| "fa-btc" | |||||
| "fa-usd" | |||||
| "fa-gbp" | |||||
| "fa-archive" | |||||
| "fa-area-chart" | |||||
| "fa-bed" | |||||
| "fa-beer" | |||||
| "fa-bell" | |||||
| "fa-binoculars" | |||||
| "fa-birthday-cake" | |||||
| "fa-bomb" | |||||
| "fa-briefcase" | |||||
| "fa-bug" | |||||
| "fa-camera" | |||||
| "fa-cart-plus" | |||||
| "fa-certificate" | |||||
| "fa-coffee" | |||||
| "fa-cloud" | |||||
| "fa-comment" | |||||
| "fa-cube" | |||||
| "fa-cutlery" | |||||
| "fa-database" | |||||
| "fa-diamond" | |||||
| "fa-exclamation-circle" | |||||
| "fa-eye" | |||||
| "fa-flag" | |||||
| "fa-flask" | |||||
| "fa-futbol-o" | |||||
| "fa-gamepad" | |||||
| "fa-graduation-cap"; | |||||
type Finger = { | |||||
icon: FingerprintIcon; | |||||
color: FingerprintColor; | |||||
}; | |||||
type Fingerprint = [Finger, Finger, Finger]; | |||||
type AccessToken = string; | |||||
type SignInRequestPayload = { | |||||
email: string; | |||||
password: string; | |||||
}; | |||||
type SignInResponsePayload = { | |||||
access: AccessToken; | |||||
}; | |||||
type GetPasswordsResponsePayload = { | |||||
results: LegacyPasswordProfile[]; | |||||
}; |
@@ -1,15 +0,0 @@ | |||||
{ | |||||
"nav": { | |||||
"settings": "settings", | |||||
"passwords": "passwords", | |||||
"signin": "sign in", | |||||
"myaccount": "my account" | |||||
}, | |||||
"settings": { | |||||
"selectlanguage": "select default language" | |||||
}, | |||||
"auth": { | |||||
"donthaveanaccount": "don't have an account?", | |||||
"alreadyhaveanaccount": "already have an account?" | |||||
} | |||||
} |
@@ -1,15 +0,0 @@ | |||||
{ | |||||
"nav": { | |||||
"settings": "options", | |||||
"passwords": "mots de passe", | |||||
"signin": "se connecter", | |||||
"myaccount": "mon compte" | |||||
}, | |||||
"settings": { | |||||
"selectlanguage": "selectionnez la langue par défault" | |||||
}, | |||||
"auth": { | |||||
"donthaveanaccount": "pas encore de compte?", | |||||
"alreadyhaveanaccount": "vous avez déjà un compte?" | |||||
} | |||||
} |
@@ -1,27 +0,0 @@ | |||||
import i18n from "i18next"; | |||||
import { initReactI18next } from "react-i18next"; | |||||
import resources from "./resources"; | |||||
import { SETTINGS_KEY } from "../services/localStore"; | |||||
import defaultSettings from "../settings/defaultSettings"; | |||||
export function initI18n(store: Store) { | |||||
const lang = window.navigator.languages | |||||
? window.navigator.languages[0] | |||||
: window.navigator.language; | |||||
let shortLang = lang; | |||||
if (shortLang.indexOf("-") !== -1) shortLang = shortLang.split("-")[0]; | |||||
if (shortLang.indexOf("_") !== -1) shortLang = shortLang.split("_")[0]; | |||||
const settings = (store.getItem(SETTINGS_KEY) as Settings) || defaultSettings; | |||||
const { language } = settings; | |||||
i18n.use(initReactI18next).init({ | |||||
resources, | |||||
lng: language || shortLang, | |||||
fallbackLng: "en", | |||||
interpolation: { | |||||
escapeValue: false, | |||||
}, | |||||
}); | |||||
return i18n; | |||||
} |
@@ -1,13 +0,0 @@ | |||||
import en_translation from "./en.json"; | |||||
import fr_translation from "./fr.json"; | |||||
const resources = { | |||||
en: { | |||||
translation: en_translation, | |||||
}, | |||||
fr: { | |||||
translation: fr_translation, | |||||
}, | |||||
}; | |||||
export default resources; |
@@ -1,48 +0,0 @@ | |||||
import { getIconLookup } from "./icons"; | |||||
test("getIconLookup", async () => { | |||||
expect(getIconLookup("fa-flask")).toEqual({ | |||||
prefix: "fas", | |||||
iconName: "flask", | |||||
}); | |||||
expect(getIconLookup("fa-birthday-cake")).toEqual({ | |||||
prefix: "fas", | |||||
iconName: "birthday-cake", | |||||
}); | |||||
}); | |||||
test("getIconLookup retro compatibility with fa4 icons", async () => { | |||||
expect(getIconLookup("fa-jpy")).toEqual({ | |||||
prefix: "fas", | |||||
iconName: "yen-sign", | |||||
}); | |||||
expect(getIconLookup("fa-eur")).toEqual({ | |||||
prefix: "fas", | |||||
iconName: "euro-sign", | |||||
}); | |||||
expect(getIconLookup("fa-usd")).toEqual({ | |||||
prefix: "fas", | |||||
iconName: "dollar-sign", | |||||
}); | |||||
expect(getIconLookup("fa-gbp")).toEqual({ | |||||
prefix: "fas", | |||||
iconName: "pound-sign", | |||||
}); | |||||
expect(getIconLookup("fa-area-chart")).toEqual({ | |||||
prefix: "fas", | |||||
iconName: "chart-area", | |||||
}); | |||||
expect(getIconLookup("fa-cutlery")).toEqual({ | |||||
prefix: "fas", | |||||
iconName: "utensils", | |||||
}); | |||||
expect(getIconLookup("fa-diamond")).toEqual({ | |||||
prefix: "fas", | |||||
iconName: "gem", | |||||
}); | |||||
expect(getIconLookup("fa-futbol-o")).toEqual({ | |||||
prefix: "fas", | |||||
iconName: "futbol", | |||||
}); | |||||
expect(getIconLookup("fa-btc")).toEqual({ prefix: "fab", iconName: "btc" }); | |||||
}); |
@@ -1,129 +0,0 @@ | |||||
import { | |||||
library, | |||||
IconName, | |||||
IconLookup, | |||||
IconDefinition, | |||||
findIconDefinition, | |||||
} from "@fortawesome/fontawesome-svg-core"; | |||||
import { faBtc } from "@fortawesome/free-brands-svg-icons"; | |||||
import { | |||||
faHashtag, | |||||
faHeart, | |||||
faHotel, | |||||
faUniversity, | |||||
faPlug, | |||||
faAmbulance, | |||||
faBus, | |||||
faCar, | |||||
faPlane, | |||||
faRocket, | |||||
faShip, | |||||
faSubway, | |||||
faTruck, | |||||
faYenSign, | |||||
faEuroSign, | |||||
faDollarSign, | |||||
faPoundSign, | |||||
faArchive, | |||||
faChartArea, | |||||
faBed, | |||||
faBeer, | |||||
faBell, | |||||
faBinoculars, | |||||
faBirthdayCake, | |||||
faBomb, | |||||
faBriefcase, | |||||
faBug, | |||||
faCamera, | |||||
faCartPlus, | |||||
faCertificate, | |||||
faCoffee, | |||||
faCloud, | |||||
faComment, | |||||
faCube, | |||||
faUtensils, | |||||
faDatabase, | |||||
faGem, | |||||
faExclamationCircle, | |||||
faEye, | |||||
faFlag, | |||||
faFlask, | |||||
faFutbol, | |||||
faGamepad, | |||||
faGraduationCap, | |||||
} from "@fortawesome/free-solid-svg-icons"; | |||||
library.add( | |||||
faHashtag, | |||||
faHeart, | |||||
faHotel, | |||||
faUniversity, | |||||
faPlug, | |||||
faAmbulance, | |||||
faBus, | |||||
faCar, | |||||
faPlane, | |||||
faRocket, | |||||
faShip, | |||||
faSubway, | |||||
faTruck, | |||||
faYenSign, | |||||
faEuroSign, | |||||
faBtc, | |||||
faDollarSign, | |||||
faPoundSign, | |||||
faArchive, | |||||
faChartArea, | |||||
faBed, | |||||
faBeer, | |||||
faBell, | |||||
faBinoculars, | |||||
faBirthdayCake, | |||||
faBomb, | |||||
faBriefcase, | |||||
faBug, | |||||
faCamera, | |||||
faCartPlus, | |||||
faCertificate, | |||||
faCoffee, | |||||
faCloud, | |||||
faCoffee, | |||||
faComment, | |||||
faCube, | |||||
faUtensils, | |||||
faDatabase, | |||||
faGem, | |||||
faExclamationCircle, | |||||
faEye, | |||||
faFlag, | |||||
faFlask, | |||||
faFutbol, | |||||
faGamepad, | |||||
faGraduationCap | |||||
); | |||||
export function getIconLookup(old_name: FingerprintIcon): IconLookup { | |||||
const incompatible_icon_names: Partial<Record< | |||||
FingerprintIcon, | |||||
IconLookup | |||||
>> = { | |||||
"fa-jpy": { prefix: "fas", iconName: "yen-sign" }, | |||||
"fa-eur": { prefix: "fas", iconName: "euro-sign" }, | |||||
"fa-usd": { prefix: "fas", iconName: "dollar-sign" }, | |||||
"fa-gbp": { prefix: "fas", iconName: "pound-sign" }, | |||||
"fa-btc": { prefix: "fab", iconName: "btc" }, | |||||
"fa-area-chart": { prefix: "fas", iconName: "chart-area" }, | |||||
"fa-cutlery": { prefix: "fas", iconName: "utensils" }, | |||||
"fa-diamond": { prefix: "fas", iconName: "gem" }, | |||||
"fa-futbol-o": { prefix: "fas", iconName: "futbol" }, | |||||
}; | |||||
if (Object.keys(incompatible_icon_names).includes(old_name)) { | |||||
return incompatible_icon_names[old_name] as IconLookup; | |||||
} | |||||
const [, ...rest] = old_name.split("-"); | |||||
return { prefix: "fas", iconName: rest.join("-") as IconName }; | |||||
} | |||||
export function getIcon(iconName: FingerprintIcon): IconDefinition { | |||||
return findIconDefinition(getIconLookup(iconName)); | |||||
} |
@@ -1,27 +0,0 @@ | |||||
import React, { Suspense } from "react"; | |||||
import ReactDOM from "react-dom"; | |||||
import { Provider } from "react-redux"; | |||||
//import "./index.css"; | |||||
import App from "./App"; | |||||
import { BrowserRouter } from "react-router-dom"; | |||||
import { I18nextProvider } from "react-i18next"; | |||||
import { initI18n } from "./i18n"; | |||||
import reduxStore from "./store"; | |||||
import initLocalStore from "./services/localStore"; | |||||
const localStore = initLocalStore(window.localStorage); | |||||
ReactDOM.render( | |||||
<React.StrictMode> | |||||
<Suspense fallback="..."> | |||||
<Provider store={reduxStore}> | |||||
<BrowserRouter> | |||||
<I18nextProvider i18n={initI18n(localStore)}> | |||||
<App store={localStore} /> | |||||
</I18nextProvider> | |||||
</BrowserRouter> | |||||
</Provider> | |||||
</Suspense> | |||||
</React.StrictMode>, | |||||
document.getElementById("root") | |||||
); |
@@ -1,78 +0,0 @@ | |||||
import React from "react"; | |||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; | |||||
import PasswordGeneratorForm from "./PasswordGeneratorForm"; | |||||
test("PasswordGeneratorForm", async () => { | |||||
const onPasswordGenerated = jest.fn(); | |||||
render( | |||||
<PasswordGeneratorForm | |||||
masterPassword="password" | |||||
onPasswordGenerated={onPasswordGenerated} | |||||
/> | |||||
); | |||||
const siteInput = screen.queryByTestId("site-input") as HTMLInputElement; | |||||
expect(siteInput.type).toBe("text"); | |||||
expect(siteInput.value).toBe(""); | |||||
fireEvent.change(siteInput, { | |||||
target: { value: "www.lesspass.com" }, | |||||
}); | |||||
const loginInput = screen.queryByTestId("login-input") as HTMLInputElement; | |||||
expect(loginInput.type).toBe("text"); | |||||
expect(loginInput.value).toBe(""); | |||||
fireEvent.change(loginInput, { | |||||
target: { value: "contact@lesspass.com" }, | |||||
}); | |||||
const uppercaseCheckbox = screen.queryByTestId( | |||||
"uppercase-checkbox" | |||||
) as HTMLInputElement; | |||||
fireEvent.click(uppercaseCheckbox); | |||||
const digitsCheckbox = screen.queryByTestId( | |||||
"digits-checkbox" | |||||
) as HTMLInputElement; | |||||
fireEvent.click(digitsCheckbox); | |||||
const symbolsCheckbox = screen.queryByTestId( | |||||
"symbols-checkbox" | |||||
) as HTMLInputElement; | |||||
fireEvent.click(symbolsCheckbox); | |||||
const lengthInput = screen.queryByTestId("length-input") as HTMLInputElement; | |||||
expect(lengthInput.type).toBe("number"); | |||||
expect(lengthInput.value).toBe("16"); | |||||
fireEvent.change(lengthInput, { | |||||
target: { value: "17" }, | |||||
}); | |||||
const counterInput = screen.queryByTestId( | |||||
"counter-input" | |||||
) as HTMLInputElement; | |||||
expect(counterInput.type).toBe("number"); | |||||
expect(counterInput.value).toBe("1"); | |||||
fireEvent.change(counterInput, { | |||||
target: { value: "2" }, | |||||
}); | |||||
const generatePasswordButton = screen.queryByTestId( | |||||
"generate-password-button" | |||||
) as HTMLInputElement; | |||||
fireEvent.click(generatePasswordButton); | |||||
await waitFor(() => | |||||
expect(onPasswordGenerated).toHaveBeenCalledWith("sfexxbwympdakofqp") | |||||
); | |||||
}); | |||||
test("site field is selected when we mount the password generator form", () => { | |||||
render( | |||||
<PasswordGeneratorForm | |||||
masterPassword="password" | |||||
onPasswordGenerated={jest.fn()} | |||||
/> | |||||
); | |||||
const siteInput = screen.queryByTestId("site-input") as HTMLInputElement; | |||||
expect(siteInput).toBe(document.activeElement); | |||||
}); |
@@ -1,141 +0,0 @@ | |||||
import React, { useState, useRef, useEffect } from "react"; | |||||
const LessPass = require("lesspass"); | |||||
const PasswordGeneratorForm = ({ | |||||
masterPassword, | |||||
onPasswordGenerated, | |||||
}: { | |||||
masterPassword: MasterPassword; | |||||
onPasswordGenerated: (generatedPassword: string) => void; | |||||
}) => { | |||||
const [site, setSite] = useState(""); | |||||
const [login, setLogin] = useState(""); | |||||
const [lowercase, setLowercase] = useState(true); | |||||
const [uppercase, setUppercase] = useState(true); | |||||
const [digits, setDigits] = useState(true); | |||||
const [symbols, setSymbols] = useState(true); | |||||
const [length, setLength] = useState(16); | |||||
const [counter, setCounter] = useState(1); | |||||
const siteInputRef = useRef<HTMLInputElement>(null); | |||||
useEffect(() => { | |||||
siteInputRef.current?.focus(); | |||||
}, []); | |||||
return ( | |||||
<form | |||||
id="password-generator-form" | |||||
data-testid="password-generator-form" | |||||
onSubmit={(event) => { | |||||
event.preventDefault(); | |||||
const passwordProfile = { | |||||
site, | |||||
login, | |||||
lowercase, | |||||
uppercase, | |||||
digits, | |||||
symbols, | |||||
length, | |||||
counter, | |||||
}; | |||||
LessPass.generatePassword(passwordProfile, masterPassword).then( | |||||
onPasswordGenerated | |||||
); | |||||
}} | |||||
> | |||||
<label htmlFor="site-input">Site</label> | |||||
<input | |||||
type="text" | |||||
id="site-input" | |||||
data-testid="site-input" | |||||
name="site" | |||||
ref={siteInputRef} | |||||
value={site} | |||||
onChange={(event) => setSite(event.target.value)} | |||||
/> | |||||
<label htmlFor="login-input">Last name</label> | |||||
<input | |||||
type="text" | |||||
id="login-input" | |||||
data-testid="login-input" | |||||
name="login" | |||||
value={login} | |||||
onChange={(event) => setLogin(event.target.value)} | |||||
/> | |||||
<fieldset> | |||||
<legend>Options</legend> | |||||
<input | |||||
type="checkbox" | |||||
id="lowercase-checkbox" | |||||
data-testid="lowercase-checkbox" | |||||
name="lowercase" | |||||
checked={lowercase} | |||||
onChange={(event) => setLowercase(event.target.checked)} | |||||
/> | |||||
<label htmlFor="lowercase-checkbox">a-z</label> | |||||
<input | |||||
type="checkbox" | |||||
id="uppercase-checkbox" | |||||
data-testid="uppercase-checkbox" | |||||
name="uppercase" | |||||
checked={uppercase} | |||||
onChange={(event) => setUppercase(event.target.checked)} | |||||
/> | |||||
<label htmlFor="uppercase-checkbox">A-Z</label> | |||||
<input | |||||
type="checkbox" | |||||
id="digits-checkbox" | |||||
data-testid="digits-checkbox" | |||||
name="digits" | |||||
checked={digits} | |||||
onChange={(event) => setDigits(event.target.checked)} | |||||
/> | |||||
<label htmlFor="digits-checkbox">0-9</label> | |||||
<input | |||||
type="checkbox" | |||||
id="symbols-checkbox" | |||||
data-testid="symbols-checkbox" | |||||
name="symbols" | |||||
checked={symbols} | |||||
onChange={(event) => setSymbols(event.target.checked)} | |||||
/> | |||||
<label htmlFor="symbols-checkbox">%!@</label> | |||||
<label htmlFor="length-input">Length</label> | |||||
<input | |||||
type="number" | |||||
id="length-input" | |||||
data-testid="length-input" | |||||
name="length" | |||||
value={length} | |||||
onChange={(event) => setLength(parseInt(event.target.value))} | |||||
/> | |||||
<label htmlFor="length-input">Counter</label> | |||||
<input | |||||
type="number" | |||||
id="counter-input" | |||||
data-testid="counter-input" | |||||
name="counter" | |||||
value={counter} | |||||
onChange={(event) => setCounter(parseInt(event.target.value))} | |||||
/> | |||||
</fieldset> | |||||
<button | |||||
type="submit" | |||||
id="generate-password-button" | |||||
data-testid="generate-password-button" | |||||
> | |||||
Generate Password | |||||
</button> | |||||
</form> | |||||
); | |||||
}; | |||||
export default PasswordGeneratorForm; |
@@ -1,26 +0,0 @@ | |||||
import React from "react"; | |||||
import PasswordGeneratorForm from "./PasswordGeneratorForm"; | |||||
const PasswordGeneratorPage = ({ | |||||
masterPassword, | |||||
setMasterPassword, | |||||
}: { | |||||
setMasterPassword: (masterPassword: MasterPassword) => void; | |||||
masterPassword: MasterPassword; | |||||
}) => ( | |||||
<> | |||||
<PasswordGeneratorForm | |||||
masterPassword={masterPassword} | |||||
onPasswordGenerated={(generatedPassword) => alert(generatedPassword)} | |||||
/> | |||||
<button | |||||
id="lock-button" | |||||
data-testid="lock-button" | |||||
onClick={() => setMasterPassword("")} | |||||
> | |||||
lock | |||||
</button> | |||||
</> | |||||
); | |||||
export default PasswordGeneratorPage; |
@@ -1,5 +0,0 @@ | |||||
import React from "react"; | |||||
const PasswordsPage = () => <div>PasswordsPage</div>; | |||||
export default PasswordsPage; |
@@ -1,63 +0,0 @@ | |||||
import configureMockStore from "redux-mock-store"; | |||||
import { mocked } from "ts-jest/utils"; | |||||
import thunk from "redux-thunk"; | |||||
import * as api from "../services/api"; | |||||
import { | |||||
PasswordsState, | |||||
initialState, | |||||
getPasswords, | |||||
getPasswordsStart, | |||||
getPasswordsSuccess, | |||||
getPasswordsFailure, | |||||
} from "./passwordsSlice"; | |||||
import { AppDispatch } from "../store"; | |||||
jest.mock("../services/api"); | |||||
const mockedApi = mocked(api, true); | |||||
const mockedStore = configureMockStore<PasswordsState, AppDispatch>([thunk]); | |||||
test("creates both getPasswordsStart and getPasswordsSuccess when getPasswords succeeds", async () => { | |||||
const responsePayload = { | |||||
results: [ | |||||
{ | |||||
id: "p1", | |||||
site: "lesspass.com", | |||||
login: "contact@lesspass.com", | |||||
lowercase: true, | |||||
uppercase: true, | |||||
numbers: true, | |||||
symbols: true, | |||||
length: 16, | |||||
counter: 1, | |||||
}, | |||||
], | |||||
}; | |||||
const store = mockedStore(initialState); | |||||
mockedApi.getPasswords.mockResolvedValueOnce(responsePayload); | |||||
await store.dispatch(getPasswords()); | |||||
const expectedActions = [ | |||||
getPasswordsStart(), | |||||
getPasswordsSuccess(responsePayload), | |||||
]; | |||||
expect(store.getActions()).toEqual(expectedActions); | |||||
}); | |||||
test("creates both getPasswordsStart and getPasswordsFailure when getPasswords fails", async () => { | |||||
const responseError = new Error("Invalid credentials"); | |||||
const store = mockedStore(initialState); | |||||
mockedApi.getPasswords.mockRejectedValueOnce(responseError); | |||||
await store.dispatch(getPasswords()); | |||||
const expectedActions = [ | |||||
getPasswordsStart(), | |||||
getPasswordsFailure({ error: responseError }), | |||||
]; | |||||
expect(store.getActions()).toEqual(expectedActions); | |||||
}); |
@@ -1,72 +0,0 @@ | |||||
import reducer, { | |||||
initialState, | |||||
getPasswordsStart, | |||||
getPasswordsSuccess, | |||||
getPasswordsFailure, | |||||
selectIsLoading, | |||||
selectError, | |||||
selectPasswords, | |||||
} from "./passwordsSlice"; | |||||
import { PayloadAction } from "@reduxjs/toolkit"; | |||||
import { RootState } from "../store"; | |||||
test("should return the initial state", () => { | |||||
const nextState = initialState; | |||||
const result = reducer(undefined, {} as PayloadAction); | |||||
expect(result).toEqual(nextState); | |||||
}); | |||||
test("should properly set loading and error state when getPasswordsStart", () => { | |||||
const nextState = reducer(initialState, getPasswordsStart()); | |||||
const rootState = { passwords: nextState } as RootState; | |||||
expect(selectPasswords(rootState)).toEqual([]); | |||||
expect(selectIsLoading(rootState)).toEqual(true); | |||||
expect(selectError(rootState)).toEqual(null); | |||||
}); | |||||
it("should properly set loading, error and state when getPasswordsSuccess", () => { | |||||
const payload = { | |||||
results: [ | |||||
{ | |||||
id: "p1", | |||||
site: "lesspass.com", | |||||
login: "contact@lesspass.com", | |||||
lowercase: true, | |||||
uppercase: true, | |||||
numbers: true, | |||||
symbols: true, | |||||
length: 16, | |||||
counter: 1, | |||||
}, | |||||
], | |||||
}; | |||||
const nextState = reducer(initialState, getPasswordsSuccess(payload)); | |||||
const rootState = { passwords: nextState } as RootState; | |||||
expect(selectPasswords(rootState)).toEqual([ | |||||
{ | |||||
id: "p1", | |||||
site: "lesspass.com", | |||||
login: "contact@lesspass.com", | |||||
lowercase: true, | |||||
uppercase: true, | |||||
digits: true, | |||||
symbols: true, | |||||
length: 16, | |||||
counter: 1, | |||||
}, | |||||
]); | |||||
expect(selectIsLoading(rootState)).toEqual(false); | |||||
expect(selectError(rootState)).toEqual(null); | |||||
}); | |||||
it("should properly set loading, error and state getPasswordsFailure", () => { | |||||
const error = new Error("Incorrect password"); | |||||
const nextState = reducer( | |||||
initialState, | |||||
getPasswordsFailure({ error: error.message }) | |||||
); | |||||
const rootState = { passwords: nextState } as RootState; | |||||
expect(selectPasswords(rootState)).toEqual([]); | |||||
expect(selectIsLoading(rootState)).toEqual(false); | |||||
expect(selectError(rootState)).toEqual(error.message); | |||||
}); |
@@ -1,70 +0,0 @@ | |||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; | |||||
import * as api from "../services/api"; | |||||
import { RootState, AppDispatch } from "../store"; | |||||
export type PasswordsState = { | |||||
isLoading: boolean; | |||||
results: PasswordProfile[]; | |||||
error: string | null; | |||||
}; | |||||
export const initialState: PasswordsState = { | |||||
isLoading: false, | |||||
results: [], | |||||
error: null, | |||||
}; | |||||
const passwordsSlice = createSlice({ | |||||
name: "passwords", | |||||
initialState, | |||||
reducers: { | |||||
getPasswordsStart(state) { | |||||
state.isLoading = true; | |||||
state.error = null; | |||||
}, | |||||
getPasswordsSuccess(state, action: PayloadAction<GetPasswordsResponsePayload>) { | |||||
const { results } = action.payload; | |||||
state.isLoading = false; | |||||
state.results = results.map((r) => ({ | |||||
id: r.id, | |||||
site: r.site, | |||||
login: r.login, | |||||
lowercase: r.lowercase, | |||||
uppercase: r.uppercase, | |||||
digits: r.numbers, | |||||
symbols: r.symbols, | |||||
length: r.length, | |||||
counter: r.counter, | |||||
})); | |||||
state.error = null; | |||||
}, | |||||
getPasswordsFailure(state, action) { | |||||
const { error } = action.payload; | |||||
state.isLoading = false; | |||||
state.results = []; | |||||
state.error = error; | |||||
}, | |||||
}, | |||||
}); | |||||
export const { | |||||
getPasswordsStart, | |||||
getPasswordsSuccess, | |||||
getPasswordsFailure, | |||||
} = passwordsSlice.actions; | |||||
export default passwordsSlice.reducer; | |||||
export const selectPasswords = (state: RootState) => state.passwords.results; | |||||
export const selectError = (state: RootState) => state.passwords.error; | |||||
export const selectIsLoading = (state: RootState) => state.passwords.isLoading; | |||||
export const getPasswords = () => async (dispatch: AppDispatch) => { | |||||
try { | |||||
dispatch(getPasswordsStart()); | |||||
const results = await api.getPasswords(); | |||||
dispatch(getPasswordsSuccess(results)); | |||||
} catch (error) { | |||||
dispatch(getPasswordsFailure({ error })); | |||||
} | |||||
}; |
@@ -1 +0,0 @@ | |||||
/// <reference types="react-scripts" /> |
@@ -1,14 +0,0 @@ | |||||
import axios from "axios"; | |||||
export function signIn(payload: SignInRequestPayload): Promise<SignInResponsePayload> { | |||||
return axios | |||||
.post("https://api.lesspass.com/api/auth/jwt/create/", payload) | |||||
.then((response) => response.data); | |||||
} | |||||
export function getPasswords(): Promise<GetPasswordsResponsePayload> { | |||||
return axios | |||||
.get("https://api.lesspass.com/api/passwords/") | |||||
.then((response) => response.data.results); | |||||
} | |||||
@@ -1,46 +0,0 @@ | |||||
import { fakeLocalStore } from "./localStore"; | |||||
test("can save an retrieve string", () => { | |||||
const localStorage = fakeLocalStore(); | |||||
localStorage.setItem("test", "string"); | |||||
expect(localStorage.getItem("test")).toBe("string"); | |||||
}); | |||||
test("can save an delete string", () => { | |||||
const localStorage = fakeLocalStore(); | |||||
localStorage.setItem("test", "string"); | |||||
expect(localStorage.getItem("test")).toBe("string"); | |||||
localStorage.removeItem("test"); | |||||
expect(localStorage.getItem("test")).toBeNull(); | |||||
}); | |||||
test("remove non existant string didnt raise an exception", () => { | |||||
const localStorage = fakeLocalStore(); | |||||
localStorage.removeItem("test"); | |||||
}); | |||||
test("can initialize local storage with string", () => { | |||||
const localStorage = fakeLocalStore({ test: "string" }); | |||||
expect(localStorage.getItem("test")).toBe("string"); | |||||
}); | |||||
test("can save an retrieve object", () => { | |||||
const localStorage = fakeLocalStore(); | |||||
localStorage.setItem("test", { hello: "world" }); | |||||
expect(localStorage.getItem("test")).toEqual({ hello: "world" }); | |||||
}); | |||||
test("can initialize local storage with object and string", () => { | |||||
const localStorage = fakeLocalStore({ | |||||
a: "string", | |||||
b: { hello: "world" }, | |||||
}); | |||||
expect(localStorage.getItem("a")).toBe("string"); | |||||
expect(localStorage.getItem("b")).toEqual({ hello: "world" }); | |||||
}); | |||||
test("can save an retrieve boolean", () => { | |||||
const localStorage = fakeLocalStore(); | |||||
localStorage.setItem("test", false); | |||||
expect(localStorage.getItem("test")).toBe(false); | |||||
}); |
@@ -1,65 +0,0 @@ | |||||
export const MASTER_PASSWORD_KEY = "masterPassword"; | |||||
export const SETTINGS_KEY = "settings"; | |||||
export const ACCESS_TOKEN = "access_token"; | |||||
export const fakeLocalStore = (initialStore?: {}) => { | |||||
class FakeLocalStore implements Store { | |||||
store: { | |||||
[key: string]: string | object | boolean; | |||||
}; | |||||
constructor(initialStore = {}) { | |||||
this.store = initialStore; | |||||
} | |||||
getItem(key: string) { | |||||
if (key in this.store) return this.store[key]; | |||||
return null; | |||||
} | |||||
setItem(key: string, value: string | object | boolean) { | |||||
this.store[key] = value; | |||||
} | |||||
removeItem(key: string) { | |||||
delete this.store[key]; | |||||
} | |||||
} | |||||
return new FakeLocalStore(initialStore); | |||||
}; | |||||
export default function initLocalStore(localStorage: Storage) { | |||||
class LocalStore implements Store { | |||||
localStorage: Storage; | |||||
constructor(initialStore: Storage) { | |||||
this.localStorage = initialStore; | |||||
} | |||||
getItem(key: string) { | |||||
const value = this.localStorage.getItem(key); | |||||
try { | |||||
if (value === null) return value; | |||||
return JSON.parse(value); | |||||
} catch (error) { | |||||
return value; | |||||
} | |||||
} | |||||
setItem(key: string, value: string | object | boolean) { | |||||
if (typeof value === "string") { | |||||
return this.localStorage.setItem(key, value); | |||||
} | |||||
if (typeof value === "object" || typeof value === "boolean") { | |||||
return this.localStorage.setItem(key, JSON.stringify(value)); | |||||
} | |||||
} | |||||
removeItem(key: string) { | |||||
return this.localStorage.removeItem(key); | |||||
} | |||||
} | |||||
return new LocalStore(localStorage); | |||||
} |
@@ -1,94 +0,0 @@ | |||||
import React from "react"; | |||||
import { render, screen, waitFor, fireEvent } from "@testing-library/react"; | |||||
import { InitApp } from "../App.test"; | |||||
import { createMemoryHistory } from "history"; | |||||
import { fakeLocalStore } from "../services/localStore"; | |||||
test("test settings page when local storage is empty", async () => { | |||||
const history = createMemoryHistory(); | |||||
render( | |||||
<InitApp | |||||
history={history} | |||||
store={fakeLocalStore({ masterPassword: "password" })} | |||||
/> | |||||
); | |||||
fireEvent.click(screen.queryByTestId("settings-link") as HTMLLinkElement); | |||||
expect(history.location.pathname).toBe("/settings"); | |||||
const useMasterPasswordForAuth = screen.queryByTestId( | |||||
"use-master-password-for-auth-checkbox" | |||||
) as HTMLInputElement; | |||||
await waitFor(() => { | |||||
expect(useMasterPasswordForAuth).toBeInTheDocument(); | |||||
expect(useMasterPasswordForAuth.type).toBe("checkbox"); | |||||
expect(useMasterPasswordForAuth.checked).toBe(true); | |||||
}); | |||||
const frRadioButton = screen.queryByTestId( | |||||
"fr-radio-button" | |||||
) as HTMLInputElement; | |||||
const enRadioButton = screen.queryByTestId( | |||||
"en-radio-button" | |||||
) as HTMLInputElement; | |||||
await waitFor(() => { | |||||
expect(enRadioButton).toBeInTheDocument(); | |||||
expect(enRadioButton.type).toBe("radio"); | |||||
expect(enRadioButton.checked).toBe(true); | |||||
expect(frRadioButton).toBeInTheDocument(); | |||||
expect(frRadioButton.type).toBe("radio"); | |||||
expect(frRadioButton.checked).toBe(false); | |||||
}); | |||||
}); | |||||
test("test settings page when local storage has saved info", async () => { | |||||
const history = createMemoryHistory(); | |||||
render( | |||||
<InitApp | |||||
history={history} | |||||
store={fakeLocalStore({ | |||||
masterPassword: "password", | |||||
settings: { useMasterPasswordForAuth: false, language: "fr" }, | |||||
})} | |||||
/> | |||||
); | |||||
fireEvent.click(screen.queryByTestId("settings-link") as HTMLLinkElement); | |||||
expect(history.location.pathname).toBe("/settings"); | |||||
const useMasterPasswordForAuth = screen.queryByTestId( | |||||
"use-master-password-for-auth-checkbox" | |||||
) as HTMLInputElement; | |||||
await waitFor(() => { | |||||
expect(useMasterPasswordForAuth).toBeInTheDocument(); | |||||
expect(useMasterPasswordForAuth.type).toBe("checkbox"); | |||||
expect(useMasterPasswordForAuth.checked).toBe(false); | |||||
}); | |||||
const frRadioButton = screen.queryByTestId( | |||||
"fr-radio-button" | |||||
) as HTMLInputElement; | |||||
const enRadioButton = screen.queryByTestId( | |||||
"en-radio-button" | |||||
) as HTMLInputElement; | |||||
await waitFor(() => { | |||||
expect(enRadioButton.checked).toBe(false); | |||||
expect(frRadioButton.checked).toBe(true); | |||||
}); | |||||
}); | |||||
test("click on option save them in local store", async () => { | |||||
const store = fakeLocalStore({ masterPassword: "password" }); | |||||
render(<InitApp store={store} />); | |||||
fireEvent.click(screen.queryByTestId("settings-link") as HTMLLinkElement); | |||||
const frRadioButton = screen.queryByTestId( | |||||
"fr-radio-button" | |||||
) as HTMLInputElement; | |||||
fireEvent.click(frRadioButton); | |||||
await waitFor(() => { | |||||
expect(store.getItem("settings")).toMatchObject({ | |||||
useMasterPasswordForAuth: true, | |||||
language: "fr", | |||||
}); | |||||
}); | |||||
}); |
@@ -1,71 +0,0 @@ | |||||
import React from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
type SettingsPageProps = { | |||||
settings: Settings; | |||||
setSettings: (settings: Settings) => void; | |||||
}; | |||||
const SettingsPage = ({ settings, setSettings }: SettingsPageProps) => { | |||||
const { t, i18n } = useTranslation(); | |||||
const changeLanguage = (lng: string) => { | |||||
i18n.changeLanguage(lng); | |||||
}; | |||||
const setLanguage = (event: React.ChangeEvent<HTMLInputElement>) => { | |||||
const language = event.target.value; | |||||
changeLanguage(language); | |||||
setSettings({ | |||||
...settings, | |||||
language, | |||||
}); | |||||
}; | |||||
return ( | |||||
<div> | |||||
<div> | |||||
<input | |||||
type="checkbox" | |||||
id="use-master-password-for-auth-checkbox" | |||||
data-testid="use-master-password-for-auth-checkbox" | |||||
name="useMasterPasswordForAuth" | |||||
checked={settings.useMasterPasswordForAuth} | |||||
onChange={(event) => | |||||
setSettings({ | |||||
...settings, | |||||
useMasterPasswordForAuth: event.target.checked, | |||||
}) | |||||
} | |||||
/> | |||||
<label htmlFor="use-master-password-for-auth-checkbox"> | |||||
{t("settings.usemymasterpassword")} | |||||
</label> | |||||
<small>{t("settings.usemymasterpassworddescription")}</small> | |||||
</div> | |||||
<div> | |||||
<input | |||||
type="radio" | |||||
id="en-radio-button" | |||||
data-testid="en-radio-button" | |||||
name="language" | |||||
value="en" | |||||
checked={i18n.language === "en"} | |||||
onChange={setLanguage} | |||||
/> | |||||
<label htmlFor="en-radio-button">en</label> | |||||
<input | |||||
type="radio" | |||||
id="fr-radio-button" | |||||
data-testid="fr-radio-button" | |||||
name="language" | |||||
value="fr" | |||||
checked={i18n.language === "fr"} | |||||
onChange={setLanguage} | |||||
/> | |||||
<label htmlFor="fr-radio-button">fr</label> | |||||
<div>{t("settings.selectlanguage")}</div> | |||||
</div> | |||||
</div> | |||||
); | |||||
}; | |||||
export default SettingsPage; |
@@ -1,7 +0,0 @@ | |||||
const defaultSettings: Settings = { | |||||
saveMasterPassword: true, | |||||
useMasterPasswordForAuth: true, | |||||
language: null, | |||||
}; | |||||
export default defaultSettings; |
@@ -1,5 +0,0 @@ | |||||
// jest-dom adds custom jest matchers for asserting on DOM nodes. | |||||
// allows you to do things like: | |||||
// expect(element).toHaveTextContent(/react/i) | |||||
// learn more: https://github.com/testing-library/jest-dom | |||||
import '@testing-library/jest-dom/extend-expect'; |
@@ -1,15 +0,0 @@ | |||||
import { configureStore, combineReducers } from "@reduxjs/toolkit"; | |||||
import auth from "./auth/authSlice"; | |||||
import passwords from "./passwords/passwordsSlice"; | |||||
const rootReducer = combineReducers({ auth, passwords }); | |||||
const store = configureStore({ | |||||
reducer: rootReducer, | |||||
}); | |||||
export type RootState = ReturnType<typeof store.getState> | |||||
export type AppDispatch = typeof store.dispatch; | |||||
export default store; |
@@ -1,8 +0,0 @@ | |||||
/* purgecss start ignore */ | |||||
@import "tailwindcss/base"; | |||||
/* @import "./base.css"; */ | |||||
@import "tailwindcss/components"; | |||||
/* @import "./components.css"; */ | |||||
/* purgecss end ignore */ | |||||
@import "tailwindcss/utilities"; |
@@ -1,104 +0,0 @@ | |||||
import React from "react"; | |||||
import { | |||||
render, | |||||
screen, | |||||
waitFor, | |||||
fireEvent, | |||||
act, | |||||
} from "@testing-library/react"; | |||||
import MasterPassword from "./MasterPassword"; | |||||
const createFingerprintPromise = Promise.resolve([ | |||||
{ | |||||
color: "#FFB5DA", | |||||
icon: "fa-flask", | |||||
}, | |||||
{ | |||||
color: "#009191", | |||||
icon: "fa-archive", | |||||
}, | |||||
{ | |||||
color: "#B5DAFE", | |||||
icon: "fa-beer", | |||||
}, | |||||
] as Fingerprint); | |||||
beforeAll(() => { | |||||
jest.useFakeTimers(); | |||||
}); | |||||
afterAll(() => { | |||||
jest.useRealTimers(); | |||||
}); | |||||
test("MasterPassword", async () => { | |||||
render( | |||||
<MasterPassword | |||||
value="password" | |||||
createFingerprint={jest.fn(() => createFingerprintPromise)} | |||||
onChange={jest.fn()} | |||||
/> | |||||
); | |||||
jest.advanceTimersByTime(500); | |||||
const masterPasswordInput = screen.queryByTestId( | |||||
"master-password-input" | |||||
) as HTMLInputElement; | |||||
expect(masterPasswordInput).toBeInTheDocument(); | |||||
expect(masterPasswordInput.type).toBe("password"); | |||||
expect(masterPasswordInput.value).toBe("password"); | |||||
expect(masterPasswordInput).toBe(document.activeElement); | |||||
await waitFor(() => { | |||||
expect(screen.queryByTitle("icon-fa-flask")).toBeInTheDocument(); | |||||
expect(screen.queryByTitle("icon-fa-archive")).toBeInTheDocument(); | |||||
expect(screen.queryByTitle("icon-fa-beer")).toBeInTheDocument(); | |||||
}); | |||||
}); | |||||
test("MasterPassword createFingerprint has been called after debounce", async () => { | |||||
const createFingerprint = jest.fn(() => createFingerprintPromise); | |||||
render( | |||||
<MasterPassword | |||||
value="password" | |||||
createFingerprint={createFingerprint} | |||||
onChange={jest.fn()} | |||||
/> | |||||
); | |||||
expect(createFingerprint).toHaveBeenCalledTimes(1); | |||||
expect(createFingerprint).not.toHaveBeenCalledWith("password"); | |||||
jest.advanceTimersByTime(500); | |||||
expect(createFingerprint).toHaveBeenCalledWith("password"); | |||||
await waitFor(() => { | |||||
expect(screen.queryByTestId("fingerprint")).toBeInTheDocument(); | |||||
expect(screen.queryByTitle("icon-fa-flask")).toBeInTheDocument(); | |||||
expect(screen.queryByTitle("icon-fa-archive")).toBeInTheDocument(); | |||||
expect(screen.queryByTitle("icon-fa-beer")).toBeInTheDocument(); | |||||
}); | |||||
}); | |||||
test("Remove fingerprint if MasterPassword is cleared", async () => { | |||||
render( | |||||
<MasterPassword | |||||
value="" | |||||
createFingerprint={jest.fn(() => createFingerprintPromise)} | |||||
onChange={jest.fn()} | |||||
/> | |||||
); | |||||
const masterPasswordInput = screen.queryByTestId( | |||||
"master-password-input" | |||||
) as HTMLInputElement; | |||||
fireEvent.change(masterPasswordInput, { | |||||
target: { value: "p" }, | |||||
}); | |||||
await waitFor(() => { | |||||
expect(screen.queryByTestId("fingerprint")).toBeInTheDocument(); | |||||
}); | |||||
fireEvent.change(masterPasswordInput, { | |||||
target: { value: "" }, | |||||
}); | |||||
act(() => { | |||||
jest.runAllTimers(); | |||||
}); | |||||
await waitFor(() => { | |||||
expect(screen.queryByTestId("fingerprint")).toBeNull(); | |||||
}); | |||||
}); |
@@ -1,72 +0,0 @@ | |||||
import React, { useEffect, useRef, useState } from "react"; | |||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | |||||
import { getIcon } from "../icons"; | |||||
type MasterPasswordProps = { | |||||
value: string; | |||||
createFingerprint: (masterPassword: MasterPassword) => Promise<Fingerprint>; | |||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | |||||
}; | |||||
const MasterPassword = ({ | |||||
value, | |||||
createFingerprint, | |||||
onChange, | |||||
}: MasterPasswordProps) => { | |||||
const [fingerprint, setFingerprint] = useState<Fingerprint | null>(null); | |||||
const masterPasswordInputRef = useRef<HTMLInputElement>(null); | |||||
useEffect(() => { | |||||
masterPasswordInputRef.current?.focus(); | |||||
}, []); | |||||
useEffect(() => { | |||||
let isSubscribed = true; | |||||
const fakeValue = Math.random().toString(36).substring(7); | |||||
createFingerprint(fakeValue).then((fakedFingerprint: Fingerprint) => { | |||||
if (isSubscribed) { | |||||
setFingerprint(fakedFingerprint); | |||||
} | |||||
}); | |||||
const setFingerprintTimer = setTimeout(() => { | |||||
if (value) { | |||||
createFingerprint(value).then(setFingerprint); | |||||
} else { | |||||
setFingerprint(null); | |||||
} | |||||
}, 500); | |||||
return () => { | |||||
isSubscribed = false; | |||||
clearTimeout(setFingerprintTimer); | |||||
}; | |||||
}, [createFingerprint, value]); | |||||
return ( | |||||
<div> | |||||
<input | |||||
id="master-password-input" | |||||
data-testid="master-password-input" | |||||
name="masterPassword" | |||||
type="password" | |||||
ref={masterPasswordInputRef} | |||||
value={value} | |||||
onChange={onChange} | |||||
/> | |||||
{fingerprint && ( | |||||
<div id="fingerprint" data-testid="fingerprint"> | |||||
<FontAwesomeIcon | |||||
title={`icon-${fingerprint[0].icon}`} | |||||
icon={getIcon(fingerprint[0].icon)} | |||||
/> | |||||
<FontAwesomeIcon | |||||
title={`icon-${fingerprint[1].icon}`} | |||||
icon={getIcon(fingerprint[1].icon)} | |||||
/> | |||||
<FontAwesomeIcon | |||||
title={`icon-${fingerprint[2].icon}`} | |||||
icon={getIcon(fingerprint[2].icon)} | |||||
/> | |||||
</div> | |||||
)} | |||||
</div> | |||||
); | |||||
}; | |||||
export default MasterPassword; |
@@ -1,51 +0,0 @@ | |||||
import React, { useEffect, useRef, useState } from "react"; | |||||
import MasterPassword from "./MasterPassword"; | |||||
import { createFingerprint } from "lesspass"; | |||||
export const UnlockPage = ({ | |||||
settings, | |||||
unlock, | |||||
}: { | |||||
settings: Settings; | |||||
unlock: (masterPassword: MasterPassword, saveMasterPassword:boolean) => void; | |||||
}) => { | |||||
const [masterPassword, setMasterPassword] = useState<MasterPassword>(""); | |||||
const [saveMasterPassword, setSaveMasterPassword] = useState(settings.saveMasterPassword); | |||||
const masterPasswordInputRef = useRef<HTMLInputElement>(null); | |||||
useEffect(() => { | |||||
masterPasswordInputRef.current?.focus(); | |||||
}, []); | |||||
return ( | |||||
<form | |||||
onSubmit={(event) => { | |||||
event.preventDefault(); | |||||
unlock(masterPassword, saveMasterPassword); | |||||
}} | |||||
> | |||||
<MasterPassword | |||||
value={masterPassword} | |||||
createFingerprint={createFingerprint} | |||||
onChange={(event) => { | |||||
event.persist(); | |||||
setMasterPassword(event.target.value); | |||||
}} | |||||
/> | |||||
<input | |||||
id="save-master-password-checkbox" | |||||
data-testid="save-master-password-checkbox" | |||||
name="saveMasterPassword" | |||||
type="checkbox" | |||||
checked={saveMasterPassword} | |||||
onChange={(event) => { | |||||
setSaveMasterPassword(event.target.checked); | |||||
}} | |||||
/> | |||||
keep master password locally <a href="#">understand the risk</a> | |||||
<button id="unlock" data-testid="unlock"> | |||||
unlock | |||||
</button> | |||||
</form> | |||||
); | |||||
}; | |||||
export default UnlockPage; |
@@ -1 +0,0 @@ | |||||
declare module "lesspass"; |
@@ -1,12 +0,0 @@ | |||||
module.exports = { | |||||
future: { | |||||
removeDeprecatedGapUtilities: true, | |||||
purgeLayersByDefault: true, | |||||
}, | |||||
purge: false, | |||||
theme: { | |||||
extend: {}, | |||||
}, | |||||
variants: {}, | |||||
plugins: [], | |||||
}; |
@@ -1,25 +0,0 @@ | |||||
{ | |||||
"compilerOptions": { | |||||
"target": "es5", | |||||
"lib": [ | |||||
"dom", | |||||
"dom.iterable", | |||||
"esnext" | |||||
], | |||||
"allowJs": true, | |||||
"skipLibCheck": true, | |||||
"esModuleInterop": true, | |||||
"allowSyntheticDefaultImports": true, | |||||
"strict": true, | |||||
"forceConsistentCasingInFileNames": true, | |||||
"module": "esnext", | |||||
"moduleResolution": "node", | |||||
"resolveJsonModule": true, | |||||
"isolatedModules": true, | |||||
"noEmit": true, | |||||
"jsx": "react" | |||||
}, | |||||
"include": [ | |||||
"src" | |||||
] | |||||
} |
@@ -1,69 +0,0 @@ | |||||
# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
# Runtime data | |||||
pids | |||||
*.pid | |||||
*.seed | |||||
*.pid.lock | |||||
# Directory for instrumented libs generated by jscoverage/JSCover | |||||
lib-cov | |||||
# Coverage directory used by tools like istanbul | |||||
coverage | |||||
# nyc test coverage | |||||
.nyc_output | |||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | |||||
.grunt | |||||
# Bower dependency directory (https://bower.io/) | |||||
bower_components | |||||
# node-waf configuration | |||||
.lock-wscript | |||||
# Compiled binary addons (http://nodejs.org/api/addons.html) | |||||
build/Release | |||||
# Dependency directories | |||||
node_modules/ | |||||
jspm_packages/ | |||||
# Typescript v1 declaration files | |||||
typings/ | |||||
# Optional npm cache directory | |||||
.npm | |||||
# Optional eslint cache | |||||
.eslintcache | |||||
# Optional REPL history | |||||
.node_repl_history | |||||
# Output of 'npm pack' | |||||
*.tgz | |||||
# dotenv environment variable files | |||||
.env* | |||||
# gatsby files | |||||
.cache/ | |||||
public | |||||
# Mac files | |||||
.DS_Store | |||||
# Yarn | |||||
yarn-error.log | |||||
.pnp/ | |||||
.pnp.js | |||||
# Yarn Integrity file | |||||
.yarn-integrity |
@@ -1,14 +0,0 @@ | |||||
FROM node:14-alpine AS builder | |||||
LABEL maintainer="LessPass <contact@lesspass.com>" | |||||
LABEL name="LessPass Frontend" | |||||
WORKDIR /opt/frontend | |||||
COPY package.json ./ | |||||
RUN yarn install | |||||
COPY . /opt/frontend | |||||
RUN yarn build:static | |||||
RUN yarn build | |||||
FROM nginx:alpine | |||||
COPY --from=builder /opt/frontend/build /usr/share/nginx/html | |||||
COPY nginx.conf /etc/nginx/conf.d/default.conf | |||||
EXPOSE 80 | |||||
CMD ["nginx", "-g", "daemon off;"] |
@@ -1 +0,0 @@ | |||||
import "./src/styles/global.css"; |
@@ -1,33 +0,0 @@ | |||||
module.exports = { | |||||
siteMetadata: { | |||||
title: `LessPass`, | |||||
description: `stateless password manager`, | |||||
author: `Guillaume Vincent`, | |||||
}, | |||||
plugins: [ | |||||
`gatsby-plugin-react-helmet`, | |||||
{ | |||||
resolve: `gatsby-source-filesystem`, | |||||
options: { | |||||
name: `images`, | |||||
path: `${__dirname}/src/images`, | |||||
}, | |||||
}, | |||||
`gatsby-transformer-sharp`, | |||||
`gatsby-plugin-sharp`, | |||||
{ | |||||
resolve: `gatsby-plugin-manifest`, | |||||
options: { | |||||
name: `gatsby-starter-default`, | |||||
short_name: `starter`, | |||||
start_url: `/`, | |||||
background_color: `#3398eb`, | |||||
theme_color: `#3398eb`, | |||||
display: `minimal-ui`, | |||||
icon: `src/images/icon.png`, | |||||
}, | |||||
}, | |||||
`gatsby-plugin-postcss`, | |||||
`@wardpeet/gatsby-plugin-static-site` | |||||
], | |||||
} |
@@ -1,46 +0,0 @@ | |||||
server { | |||||
listen 80; | |||||
server_name frontend; | |||||
server_tokens off; | |||||
#charset koi8-r; | |||||
#access_log /var/log/nginx/host.access.log main; | |||||
location / { | |||||
root /usr/share/nginx/html; | |||||
index index.html index.htm; | |||||
try_files $uri /index.html; | |||||
} | |||||
#error_page 404 /404.html; | |||||
# redirect server error pages to the static page /50x.html | |||||
# | |||||
error_page 500 502 503 504 /50x.html; | |||||
location = /50x.html { | |||||
root /usr/share/nginx/html; | |||||
} | |||||
# proxy the PHP scripts to Apache listening on 127.0.0.1:80 | |||||
# | |||||
#location ~ \.php$ { | |||||
# proxy_pass http://127.0.0.1; | |||||
#} | |||||
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 | |||||
# | |||||
#location ~ \.php$ { | |||||
# root html; | |||||
# fastcgi_pass 127.0.0.1:9000; | |||||
# fastcgi_index index.php; | |||||
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; | |||||
# include fastcgi_params; | |||||
#} | |||||
# deny access to .htaccess files, if Apache's document root | |||||
# concurs with nginx's one | |||||
# | |||||
#location ~ /\.ht { | |||||
# deny all; | |||||
#} | |||||
} |
@@ -1,46 +0,0 @@ | |||||
{ | |||||
"name": "lesspass-website", | |||||
"description": "LessPass web site", | |||||
"version": "0.1.0", | |||||
"license": "GPL-3.0", | |||||
"author": "Guillaume Vincent <guillaume@oslab.fr>", | |||||
"dependencies": { | |||||
"@wardpeet/gatsby-plugin-static-site": "^0.3.0", | |||||
"gatsby": "^4.6.1", | |||||
"gatsby-image": "^3.11.0", | |||||
"gatsby-plugin-manifest": "^4.6.0", | |||||
"gatsby-plugin-offline": "^5.6.0", | |||||
"gatsby-plugin-postcss": "^5.6.0", | |||||
"gatsby-plugin-react-helmet": "^5.6.0", | |||||
"gatsby-plugin-sharp": "^4.6.0", | |||||
"gatsby-source-filesystem": "^4.6.0", | |||||
"gatsby-transformer-sharp": "^4.6.0", | |||||
"lesspass-pure": "^9.5.6", | |||||
"prettier": "2.5.1", | |||||
"prop-types": "^15.8.1", | |||||
"react": "^17.0.2", | |||||
"react-dom": "^17.0.2", | |||||
"react-helmet": "^6.1.0", | |||||
"tailwindcss": "^3.0.18" | |||||
}, | |||||
"devDependencies": { | |||||
"@types/react-helmet": "^6.1.5" | |||||
}, | |||||
"keywords": [ | |||||
"lesspass" | |||||
], | |||||
"scripts": { | |||||
"no-analytics": "gatsby telemetry --disable", | |||||
"build:static": "cp -r node_modules/lesspass-pure/dist/* static/", | |||||
"build": "yarn no-analytics && gatsby build", | |||||
"develop": "gatsby develop", | |||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"", | |||||
"start": "yarn no-analytics && yarn develop", | |||||
"serve": "gatsby serve", | |||||
"clean": "gatsby clean", | |||||
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" | |||||
}, | |||||
"engines": { | |||||
"node": ">=14.15.0" | |||||
} | |||||
} |
@@ -1,3 +0,0 @@ | |||||
module.exports = () => ({ | |||||
plugins: [require("tailwindcss")], | |||||
}); |
@@ -1 +0,0 @@ | |||||
declare module "*.png"; |
@@ -1,46 +0,0 @@ | |||||
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40"> | |||||
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title> | |||||
<g> | |||||
<g> | |||||
<g> | |||||
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/> | |||||
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/> | |||||
</g> | |||||
<g id="_Group_" data-name="<Group>"> | |||||
<g id="_Group_2" data-name="<Group>"> | |||||
<g id="_Group_3" data-name="<Group>"> | |||||
<path id="_Path_" data-name="<Path>" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/> | |||||
<path id="_Path_2" data-name="<Path>" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/> | |||||
</g> | |||||
</g> | |||||
<g> | |||||
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/> | |||||
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/> | |||||
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/> | |||||
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/> | |||||
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/> | |||||
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/> | |||||
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/> | |||||
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/> | |||||
</g> | |||||
</g> | |||||
</g> | |||||
<g id="_Group_4" data-name="<Group>"> | |||||
<g> | |||||
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/> | |||||
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/> | |||||
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/> | |||||
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/> | |||||
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/> | |||||
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/> | |||||
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/> | |||||
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/> | |||||
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/> | |||||
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/> | |||||
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/> | |||||
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/> | |||||
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/> | |||||
</g> | |||||
</g> | |||||
</g> | |||||
</svg> |
@@ -1,6 +0,0 @@ | |||||
import React from "react" | |||||
import { PageProps } from "gatsby" | |||||
export default function NotFoundPage(props: PageProps) { | |||||
return <div>404</div> | |||||
} |
@@ -1,308 +0,0 @@ | |||||
import React, { useEffect } from "react"; | |||||
import { withPrefix } from "gatsby"; | |||||
import Helmet from "react-helmet"; | |||||
import GooglePlayBadge from "../images/google-play-badge.png"; | |||||
import ApplePlayBadge from "../images/download-on-the-App-Store.png"; | |||||
import FDroidBadge from "../images/get-it-on-fdroid.png"; | |||||
import ChromeBadge from "../images/get-chrome-badge.png"; | |||||
import AMOBadge from "../images/get-amo-badge.png"; | |||||
import CLIBadge from "../images/cli-badge.png"; | |||||
import HowItWorks from "../images/HowItWorks.png"; | |||||
export default function IndexPage() { | |||||
const currentYear = new Date().getFullYear(); | |||||
useEffect(() => { | |||||
const script = document.createElement("script"); | |||||
script.src = withPrefix("lesspass.min.js"); | |||||
script.async = true; | |||||
document.body.appendChild(script); | |||||
return () => { | |||||
document.body.removeChild(script); | |||||
}; | |||||
}, []); | |||||
return ( | |||||
<> | |||||
<div className="relative bg-gray-800 overflow-hidden"> | |||||
<Helmet> | |||||
<link href="lesspass.min.css" rel="stylesheet" /> | |||||
</Helmet> | |||||
<div className="relative pt-6 pb-16 sm:pb-24"> | |||||
<main className="mt-16 sm:mt-24"> | |||||
<div className="mx-auto max-w-7xl"> | |||||
<div className="lg:grid lg:grid-cols-12 lg:gap-8"> | |||||
<div className="px-4 sm:px-6 sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left lg:flex lg:items-center"> | |||||
<div> | |||||
<h1 className="mt-4 text-4xl tracking-tight font-extrabold text-white sm:mt-5 sm:leading-none lg:mt-6 lg:text-5xl xl:text-6xl"> | |||||
<span className="md:block">LessPass</span>{" "} | |||||
<span className="md:block"> | |||||
stateless password manager | |||||
</span> | |||||
</h1> | |||||
<p className="mt-3 text-base text-gray-300 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl"> | |||||
Stop wasting your time synchronizing your encrypted vault. | |||||
Remember one master password to access your passwords, | |||||
anywhere, anytime, from any device. No sync needed. | |||||
</p> | |||||
</div> | |||||
</div> | |||||
<div className="mt-16 sm:mt-24 lg:mt-0 lg:col-span-6"> | |||||
<div className="bg-white sm:max-w-md sm:w-full sm:mx-auto sm:rounded-lg sm:overflow-hidden"> | |||||
<div | |||||
style={{ minHeight: "449px" }} | |||||
className="lesspass--unbordered lesspass--full-width" | |||||
> | |||||
<div id="lesspass"></div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</main> | |||||
</div> | |||||
</div> | |||||
<div className="py-16 bg-white overflow-hidden lg:py-24"> | |||||
<div className="relative max-w-xl mx-auto px-4 sm:px-6 lg:px-8 lg:max-w-7xl"> | |||||
<div className="relative mt-12 lg:mt-24 lg:grid lg:grid-cols-2 lg:gap-8 lg:items-center"> | |||||
<div className="relative"> | |||||
<h3 className="text-2xl font-extrabold text-gray-900 tracking-tight sm:text-3xl"> | |||||
Compute your password offline | |||||
</h3> | |||||
<p className="mt-3 text-lg text-gray-500"> | |||||
LessPass computes a unique password using a site, login, and a | |||||
master password. You don't need to sync a password vault across | |||||
every device or to the cloud, because LessPass works offline! | |||||
</p> | |||||
</div> | |||||
<div className="mt-10 -mx-4 relative lg:mt-0" aria-hidden="true"> | |||||
<svg | |||||
className="absolute left-1/2 transform -translate-x-1/2 translate-y-16 lg:hidden" | |||||
width={784} | |||||
height={404} | |||||
fill="none" | |||||
viewBox="0 0 784 404" | |||||
> | |||||
<defs> | |||||
<pattern | |||||
id="ca9667ae-9f92-4be7-abcb-9e3d727f2941" | |||||
x={0} | |||||
y={0} | |||||
width={20} | |||||
height={20} | |||||
patternUnits="userSpaceOnUse" | |||||
> | |||||
<rect | |||||
x={0} | |||||
y={0} | |||||
width={4} | |||||
height={4} | |||||
className="text-gray-200" | |||||
fill="currentColor" | |||||
/> | |||||
</pattern> | |||||
</defs> | |||||
<rect | |||||
width={784} | |||||
height={404} | |||||
fill="url(#ca9667ae-9f92-4be7-abcb-9e3d727f2941)" | |||||
/> | |||||
</svg> | |||||
<img | |||||
className="relative mx-auto" | |||||
width={490} | |||||
src={HowItWorks} | |||||
alt="How it works" | |||||
/> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div className="bg-white"> | |||||
<div className="max-w-7xl mx-auto py-12 px-4 sm:py-16 sm:px-6 lg:px-8 lg:py-20"> | |||||
<div className="max-w-4xl mx-auto text-center"> | |||||
<h2 className="text-3xl font-extrabold text-gray-900 sm:text-4xl"> | |||||
Open Source | |||||
</h2> | |||||
<p className="mt-3 text-xl text-gray-500 sm:mt-4"> | |||||
To us, a password manager is only as secure as it is transparent. | |||||
That's why LessPass is fully open source. Don't just take our word | |||||
for it, take a peek under the hood and see for yourself! | |||||
</p> | |||||
</div> | |||||
<dl className="mt-10 text-center sm:max-w-3xl sm:mx-auto sm:grid sm:grid-cols-3 sm:gap-8"> | |||||
<div className="flex flex-col"> | |||||
<dt className="order-2 mt-2 text-lg leading-6 font-medium text-gray-500"> | |||||
Commits | |||||
</dt> | |||||
<dd className="order-1 text-5xl font-extrabold text-gray-900"> | |||||
2200+ | |||||
</dd> | |||||
</div> | |||||
<div className="flex flex-col mt-10 sm:mt-0"> | |||||
<dt className="order-2 mt-2 text-lg leading-6 font-medium text-gray-500"> | |||||
Contributors | |||||
</dt> | |||||
<dd className="order-1 text-5xl font-extrabold text-gray-900"> | |||||
50+ | |||||
</dd> | |||||
</div> | |||||
<div className="flex flex-col mt-10 sm:mt-0"> | |||||
<dt className="order-2 mt-2 text-lg leading-6 font-medium text-gray-500"> | |||||
Stars | |||||
</dt> | |||||
<dd className="order-1 text-5xl font-extrabold text-gray-900"> | |||||
5k | |||||
</dd> | |||||
</div> | |||||
</dl> | |||||
</div> | |||||
<div className="bg-white"> | |||||
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8"> | |||||
<h2 className="text-center text-3xl font-extrabold text-gray-900 sm:text-4xl"> | |||||
Use it everywhere | |||||
</h2> | |||||
<div className="mt-6 grid grid-cols-2 gap-0.5 md:grid-cols-3 lg:mt-8"> | |||||
<div className="col-span-1 flex justify-center py-8 px-8"> | |||||
<a href="https://play.google.com/store/apps/details?id=com.lesspass.android&hl=en"> | |||||
<img | |||||
className="max-h-20" | |||||
src={GooglePlayBadge} | |||||
alt="Google Play badge" | |||||
/> | |||||
</a> | |||||
</div> | |||||
<div className="col-span-1 flex justify-center py-8 px-8"> | |||||
<a href="https://apps.apple.com/app/id1531215924"> | |||||
<img | |||||
className="max-h-20" | |||||
src={ApplePlayBadge} | |||||
alt="Apple Play badge" | |||||
/> | |||||
</a> | |||||
</div> | |||||
<div className="col-span-1 flex justify-center py-8 px-8"> | |||||
<a href="https://f-droid.org/en/packages/com.lesspass.android/"> | |||||
<img | |||||
className="max-h-20" | |||||
src={FDroidBadge} | |||||
alt="FDroid badge" | |||||
/> | |||||
</a> | |||||
</div> | |||||
<div className="col-span-1 flex justify-center py-8 px-8"> | |||||
<a href="https://chrome.google.com/webstore/detail/lesspass/lcmbpoclaodbgkbjafnkbbinogcbnjih"> | |||||
<img | |||||
className="max-h-20" | |||||
src={ChromeBadge} | |||||
alt="Chrome badge" | |||||
/> | |||||
</a> | |||||
</div> | |||||
<div className="col-span-1 flex justify-center py-8 px-8"> | |||||
<a href="https://addons.mozilla.org/en-US/firefox/addon/lesspass/"> | |||||
<img className="max-h-16" src={AMOBadge} alt="AMO badge" /> | |||||
</a> | |||||
</div> | |||||
<div className="col-span-1 flex justify-center py-8 px-8"> | |||||
<a href="https://github.com/lesspass/lesspass#cli"> | |||||
<img className="max-h-16" src={CLIBadge} alt="Cli badge" /> | |||||
</a> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div className="bg-white pt-16 pb-20 px-4 sm:px-6 lg:pt-24 lg:pb-28 lg:px-8"> | |||||
<div className="relative max-w-lg mx-auto divide-y-2 divide-gray-200 lg:max-w-7xl"> | |||||
<div> | |||||
<h2 className="text-3xl tracking-tight font-extrabold text-gray-900 sm:text-4xl"> | |||||
Recent publications | |||||
</h2> | |||||
<p className="mt-3 text-xl text-gray-500 sm:mt-4"> | |||||
Read the latest blog posts from LessPass | |||||
</p> | |||||
</div> | |||||
<div className="mt-12 grid gap-16 pt-12 lg:grid-cols-3 lg:gap-x-5 lg:gap-y-12"> | |||||
<div> | |||||
<div> | |||||
<span className="bg-indigo-100 text-indigo-800 inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium"> | |||||
article | |||||
</span> | |||||
</div> | |||||
<a | |||||
href="https://blog.lesspass.com/2016-11-10/why-lesspass-change-license" | |||||
className="block mt-4" | |||||
> | |||||
<p className="text-xl font-semibold text-gray-900"> | |||||
Why LessPass changed its license? | |||||
</p> | |||||
<p className="mt-3 text-base text-gray-500"> | |||||
An open community starts with a good license. That’s why we | |||||
decided to change LessPass license from MIT to GPLv3. | |||||
</p> | |||||
</a> | |||||
</div> | |||||
<div> | |||||
<div> | |||||
<span className="bg-indigo-100 text-indigo-800 inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium"> | |||||
article | |||||
</span> | |||||
</div> | |||||
<a | |||||
href="https://blog.lesspass.com/2016-10-19/how-does-it-work" | |||||
className="block mt-4" | |||||
> | |||||
<p className="text-xl font-semibold text-gray-900"> | |||||
LessPass How Does It Work? | |||||
</p> | |||||
<p className="mt-3 text-base text-gray-500"> | |||||
Managing your Internet passwords is not easy. You probably use | |||||
a password manager to help you. The system is simple, the tool | |||||
generates random passwords whenever you need them and saves | |||||
them into a file protected with a strong password. This system | |||||
is very robust, you only need to remember one password to rule | |||||
them all! Now you have a unique password for each site on the | |||||
Internet. | |||||
</p> | |||||
</a> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<footer className="bg-white"> | |||||
<div className="max-w-7xl mx-auto py-12 px-4 overflow-hidden sm:px-6 lg:px-8"> | |||||
<div className="mt-8 flex justify-center space-x-6"> | |||||
<a | |||||
href="https://twitter.com/guillaume20100" | |||||
className="text-gray-400 hover:text-gray-500" | |||||
> | |||||
<span className="sr-only">Twitter</span> | |||||
<svg fill="currentColor" viewBox="0 0 24 24" className="h-6 w-6"> | |||||
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" /> | |||||
</svg> | |||||
</a> | |||||
<a | |||||
href="https://github.com/lesspass/lesspass/" | |||||
className="text-gray-400 hover:text-gray-500" | |||||
> | |||||
<span className="sr-only">Github</span> | |||||
<svg fill="currentColor" viewBox="0 0 24 24" className="h-6 w-6"> | |||||
<path | |||||
fillRule="evenodd" | |||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" | |||||
clipRule="evenodd" | |||||
/> | |||||
</svg> | |||||
</a> | |||||
</div> | |||||
<p className="mt-8 text-center text-base text-gray-400"> | |||||
© 2015-{currentYear} LessPass, Inc. All rights reserved. | |||||
</p> | |||||
</div> | |||||
</footer> | |||||
</> | |||||
); | |||||
} |
@@ -1,3 +0,0 @@ | |||||
@tailwind base; | |||||
@tailwind components; | |||||
@tailwind utilities; |
@@ -1,7 +0,0 @@ | |||||
module.exports = { | |||||
content: ["./src/**/*.{js,jsx,ts,tsx}"], | |||||
theme: { | |||||
extend: {}, | |||||
}, | |||||
plugins: [], | |||||
}; |
@@ -1,69 +0,0 @@ | |||||
{ | |||||
"compilerOptions": { | |||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */ | |||||
/* Basic Options */ | |||||
// "incremental": true, /* Enable incremental compilation */ | |||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ | |||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ | |||||
// "lib": [], /* Specify library files to be included in the compilation. */ | |||||
// "allowJs": true, /* Allow javascript files to be compiled. */ | |||||
// "checkJs": true, /* Report errors in .js files. */ | |||||
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ | |||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */ | |||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ | |||||
// "sourceMap": true, /* Generates corresponding '.map' file. */ | |||||
// "outFile": "./", /* Concatenate and emit output to single file. */ | |||||
// "outDir": "./", /* Redirect output structure to the directory. */ | |||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ | |||||
// "composite": true, /* Enable project compilation */ | |||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ | |||||
// "removeComments": true, /* Do not emit comments to output. */ | |||||
// "noEmit": true, /* Do not emit outputs. */ | |||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */ | |||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ | |||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ | |||||
/* Strict Type-Checking Options */ | |||||
"strict": true, /* Enable all strict type-checking options. */ | |||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ | |||||
// "strictNullChecks": true, /* Enable strict null checks. */ | |||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ | |||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ | |||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ | |||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ | |||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ | |||||
/* Additional Checks */ | |||||
// "noUnusedLocals": true, /* Report errors on unused locals. */ | |||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */ | |||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ | |||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ | |||||
/* Module Resolution Options */ | |||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ | |||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ | |||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ | |||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ | |||||
// "typeRoots": [], /* List of folders to include type definitions from. */ | |||||
// "types": [], /* Type declaration files to be included in compilation. */ | |||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ | |||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ | |||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ | |||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ | |||||
/* Source Map Options */ | |||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ | |||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ | |||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ | |||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ | |||||
/* Experimental Options */ | |||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ | |||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ | |||||
/* Advanced Options */ | |||||
"skipLibCheck": true, /* Skip type checking of declaration files. */ | |||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ | |||||
} | |||||
} |
@@ -1,98 +0,0 @@ | |||||
from secrets import token_hex | |||||
from lesspass import password | |||||
def generate_random_entropy(): | |||||
return int(token_hex(32), 16) | |||||
CHARACTER_SUBSETS = { | |||||
"lowercase": "abcdefghijklmnopqrstuvwxyz", | |||||
"uppercase": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | |||||
"digits": "0123456789", | |||||
"symbols": "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", | |||||
} | |||||
def reverse_entropy(entropy, remainder, modulo): | |||||
return entropy - modulo - remainder | |||||
def remove_at(i, s): | |||||
return s[:i] + s[i + 1 :] | |||||
def get_rule_len(rule): | |||||
if rule == "lowercase": | |||||
return 26 | |||||
if rule == "digits": | |||||
return 10 | |||||
rules = [ | |||||
"lowercase", | |||||
"digits", | |||||
] | |||||
old_password = "bar4" | |||||
chars_remaining = "%s" % old_password | |||||
one_char_per_rule = [] | |||||
for rule in reversed(rules): | |||||
for i, c in enumerate(old_password): | |||||
print(f"char {c} index {i}") | |||||
rule_pool = CHARACTER_SUBSETS[rule] | |||||
if c in rule_pool: | |||||
print(f"{c} in {rule_pool}") | |||||
char_per_rule = (c, rule, i) | |||||
print(char_per_rule) | |||||
one_char_per_rule.append(char_per_rule) | |||||
chars_remaining = remove_at(i, chars_remaining) | |||||
break | |||||
else: | |||||
print(f"{c} not in {rule_pool}. skipping") | |||||
pass | |||||
print("-------") | |||||
print("chars to insert:", one_char_per_rule) | |||||
entropy = 654762009171870546215294809657 | |||||
j = 7 | |||||
print(f"e{j}:", entropy) | |||||
j -= 1 | |||||
for i, rule in enumerate(one_char_per_rule): | |||||
print(f"e * ({len(old_password) - i - 1}) + {rule[2]}") | |||||
entropy = entropy * (len(old_password) - i - 1) + rule[2] | |||||
print(f"e{j}:", entropy) | |||||
j -= 1 | |||||
for i, rule in enumerate(one_char_per_rule): | |||||
_pool_of_char = CHARACTER_SUBSETS[rule[1]] | |||||
print("len _pool_of_char:", len(_pool_of_char)) | |||||
position = _pool_of_char.find(rule[0]) | |||||
print("position:", position) | |||||
entropy = entropy * len(_pool_of_char) + position | |||||
print(f"e{j}:", entropy) | |||||
j -= 1 | |||||
pool_of_char = CHARACTER_SUBSETS["lowercase"] + CHARACTER_SUBSETS["digits"] | |||||
# print(chars_remaining) | |||||
while chars_remaining != "": | |||||
last_char = chars_remaining[-1] | |||||
chars_remaining = chars_remaining[:-1] | |||||
entropy = entropy * len(pool_of_char) + pool_of_char.find(last_char) | |||||
print(f"e{j}:", entropy) | |||||
j -= 1 | |||||
print(entropy) | |||||
print("x" * 100) | |||||
print( | |||||
password._render_password( | |||||
entropy, | |||||
{ | |||||
"lowercase": True, | |||||
"uppercase": False, | |||||
"digits": True, | |||||
"symbols": False, | |||||
"length": 5, | |||||
}, | |||||
) | |||||
) |