diff --git a/packages/dashboard-v2/README.md b/packages/dashboard-v2/README.md
index ba9db568..ab0421f8 100644
--- a/packages/dashboard-v2/README.md
+++ b/packages/dashboard-v2/README.md
@@ -8,3 +8,18 @@ This is a Gatsby application. To run it locally, all you need is:
- `yarn install`
- `yarn start`
+
+## Accessing remote APIs
+
+To be able to log in on a local environment with your production credentials, you'll need to make the browser believe you're actually on the same domain, otherwise the browser will block the session cookie.
+
+To do the trick, edit your `/etc/hosts` file and add a record like this:
+
+```
+127.0.0.1 local.skynetpro.net
+```
+
+then run `yarn develop:secure` -- it will run `gatsby develop` with `--https --host=local.skynetpro.net -p=443` options.
+If you're on macOS, you may need to `sudo` the command to successfully bind to port `443`.
+
+> **NOTE:** This should become easier once we have a docker image for the new dashboard.
diff --git a/packages/dashboard-v2/package.json b/packages/dashboard-v2/package.json
index 7b561154..44f7595d 100644
--- a/packages/dashboard-v2/package.json
+++ b/packages/dashboard-v2/package.json
@@ -9,6 +9,7 @@
],
"scripts": {
"develop": "gatsby develop",
+ "develop:secure": "gatsby develop --https --host=local.skynetpro.net -p=443",
"start": "gatsby develop",
"build": "gatsby build",
"serve": "gatsby serve",
@@ -24,9 +25,11 @@
"classnames": "^2.3.1",
"copy-text-to-clipboard": "^3.0.1",
"dayjs": "^1.10.8",
+ "formik": "^2.2.9",
"gatsby": "^4.6.2",
"gatsby-plugin-postcss": "^5.7.0",
"http-status-codes": "^2.2.0",
+ "ky": "^0.30.0",
"nanoid": "^3.3.1",
"path-browserify": "^1.0.1",
"postcss": "^8.4.6",
@@ -38,7 +41,8 @@
"react-use": "^17.3.2",
"skynet-js": "^3.0.2",
"swr": "^1.2.2",
- "tailwindcss": "^3.0.23"
+ "tailwindcss": "^3.0.23",
+ "yup": "^0.32.11"
},
"devDependencies": {
"@babel/core": "^7.17.4",
diff --git a/packages/dashboard-v2/src/components/Button/Button.js b/packages/dashboard-v2/src/components/Button/Button.js
index 48cedb15..328d52cd 100644
--- a/packages/dashboard-v2/src/components/Button/Button.js
+++ b/packages/dashboard-v2/src/components/Button/Button.js
@@ -5,8 +5,8 @@ import styled from "styled-components";
/**
* Primary UI component for user interaction
*/
-export const Button = styled.button.attrs(({ disabled, $primary }) => ({
- type: "button",
+export const Button = styled.button.attrs(({ disabled, $primary, type }) => ({
+ type,
className: cn("px-6 py-2.5 rounded-full font-sans uppercase text-xs tracking-wide transition-[opacity_filter]", {
"bg-primary text-palette-600": $primary,
"bg-white border-2 border-black text-palette-600": !$primary,
@@ -14,6 +14,7 @@ export const Button = styled.button.attrs(({ disabled, $primary }) => ({
"hover:brightness-90": !disabled,
}),
}))``;
+
Button.propTypes = {
/**
* Is this the principal call to action on the page?
@@ -23,9 +24,14 @@ Button.propTypes = {
* Prevent interaction on the button
*/
disabled: PropTypes.bool,
+ /**
+ * Type of button (button / submit)
+ */
+ type: PropTypes.oneOf(["button", "submit"]),
};
Button.defaultProps = {
$primary: false,
disabled: false,
+ type: "button",
};
diff --git a/packages/dashboard-v2/src/components/Form/TextField.js b/packages/dashboard-v2/src/components/Form/TextField.js
new file mode 100644
index 00000000..7eb811a8
--- /dev/null
+++ b/packages/dashboard-v2/src/components/Form/TextField.js
@@ -0,0 +1,56 @@
+import PropTypes from "prop-types";
+import cn from "classnames";
+import { Field } from "formik";
+
+export const TextField = ({ id, label, name, error, touched, ...props }) => {
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {touched && error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
+/** Besides noted properties, it accepts all props accepted by:
+ * - a regular element
+ * - Formik's component
+ */
+TextField.propTypes = {
+ /**
+ * ID for the field. Used to couple and elements
+ */
+ id: PropTypes.string,
+ /**
+ * Label for the field
+ */
+ label: PropTypes.string,
+ /**
+ * Name of the field
+ */
+ name: PropTypes.string.isRequired,
+ /**
+ * Validation error message
+ */
+ error: PropTypes.string,
+ /**
+ * Indicates wether or not the user touched the field already.
+ */
+ touched: PropTypes.bool,
+};
diff --git a/packages/dashboard-v2/src/components/Form/index.js b/packages/dashboard-v2/src/components/Form/index.js
new file mode 100644
index 00000000..79aaa711
--- /dev/null
+++ b/packages/dashboard-v2/src/components/Form/index.js
@@ -0,0 +1 @@
+export * from "./TextField";
diff --git a/packages/dashboard-v2/src/components/HighlightedLink.js b/packages/dashboard-v2/src/components/HighlightedLink.js
new file mode 100644
index 00000000..5fa6b079
--- /dev/null
+++ b/packages/dashboard-v2/src/components/HighlightedLink.js
@@ -0,0 +1,6 @@
+import { Link } from "gatsby";
+import styled from "styled-components";
+
+export default styled(Link).attrs({
+ className: "text-primary underline-offset-3 decoration-dotted hover:text-primary-light hover:underline",
+})``;
diff --git a/packages/dashboard-v2/src/components/NavBar/NavBar.js b/packages/dashboard-v2/src/components/NavBar/NavBar.js
index 1db72dda..65d9afe5 100644
--- a/packages/dashboard-v2/src/components/NavBar/NavBar.js
+++ b/packages/dashboard-v2/src/components/NavBar/NavBar.js
@@ -1,4 +1,4 @@
-import { Link } from "gatsby";
+import { Link, navigate } from "gatsby";
import styled from "styled-components";
import { screen } from "../../lib/cssHelpers";
@@ -7,6 +7,7 @@ import { CogIcon, LockClosedIcon, SkynetLogoIcon } from "../Icons";
import { PageContainer } from "../PageContainer";
import { NavBarLink, NavBarSection } from ".";
+import accountsService from "../../services/accountsService";
const NavBarContainer = styled.div.attrs({
className: `grid sticky top-0 bg-white z-10 shadow-sm`,
@@ -77,9 +78,13 @@ export const NavBar = () => (
partiallyActive
/>
{
+ await accountsService.post("logout");
+ navigate("/auth/login");
+ // TODO: handle errors
+ }}
activeClassName="text-primary"
+ className="cursor-pointer"
icon={LockClosedIcon}
label="Log out"
/>
diff --git a/packages/dashboard-v2/src/components/forms/LoginForm.js b/packages/dashboard-v2/src/components/forms/LoginForm.js
new file mode 100644
index 00000000..61973215
--- /dev/null
+++ b/packages/dashboard-v2/src/components/forms/LoginForm.js
@@ -0,0 +1,95 @@
+import { useState } from "react";
+import PropTypes from "prop-types";
+import { Formik, Form } from "formik";
+import { Link } from "gatsby";
+import * as Yup from "yup";
+
+import HighlightedLink from "../HighlightedLink";
+import { TextField } from "../Form/TextField";
+import { Button } from "../Button";
+
+import accountsService from "../../services/accountsService";
+
+const loginSchema = Yup.object().shape({
+ email: Yup.string().required("Email is required").email("Please provide a valid email address"),
+ password: Yup.string().required("Password is required"),
+});
+
+const INVALID_CREDENTIALS_ERRORS = ["password mismatch", "user not found"];
+
+export const LoginForm = ({ onSuccess }) => {
+ const [error, setError] = useState(null);
+
+ return (
+ {
+ try {
+ await accountsService.post("login", {
+ json: values,
+ });
+
+ onSuccess();
+ } catch (err) {
+ if (err.response) {
+ const data = await err.response.json();
+
+ if (INVALID_CREDENTIALS_ERRORS.includes(data.message)) {
+ setError("Invalid email address or password.");
+ } else {
+ setError(data.message);
+ }
+ } else {
+ setError("An error occured when logging you in. Please try again.");
+ }
+ }
+ }}
+ >
+ {({ errors, touched }) => (
+
+ )}
+
+ );
+};
+
+LoginForm.propTypes = {
+ onSuccess: PropTypes.func.isRequired,
+};
diff --git a/packages/dashboard-v2/src/components/forms/index.js b/packages/dashboard-v2/src/components/forms/index.js
new file mode 100644
index 00000000..1cad6f41
--- /dev/null
+++ b/packages/dashboard-v2/src/components/forms/index.js
@@ -0,0 +1 @@
+export * from "./LoginForm";
diff --git a/packages/dashboard-v2/src/layouts/AuthLayout.js b/packages/dashboard-v2/src/layouts/AuthLayout.js
new file mode 100644
index 00000000..9706c83b
--- /dev/null
+++ b/packages/dashboard-v2/src/layouts/AuthLayout.js
@@ -0,0 +1,45 @@
+import * as React from "react";
+import styled from "styled-components";
+import { SWRConfig } from "swr";
+
+import { UserProvider } from "../contexts/user";
+import { guestsOnly } from "../lib/swrConfig";
+
+const Layout = styled.div.attrs({
+ className: "h-screen w-screen bg-black flex",
+})`
+ background-image: url(/images/auth-bg.svg);
+ background-repeat: no-repeat;
+ background-position: center center;
+`;
+
+const SloganContainer = styled.div.attrs({
+ className: "hidden md:flex lg:w-7/12 grow justify-center items-center relative overflow-hidden",
+})``;
+
+const Content = styled.div.attrs({
+ className: "w-full md:w-5/12 md:max-w-[680px] shrink-0",
+})``;
+
+const AuthLayout = ({ children }) => {
+ return (
+ <>
+
+
+
+
+
+
+ The decentralized revolution starts with decentralized storage
+
+
+
+ {children}
+
+
+
+ >
+ );
+};
+
+export default AuthLayout;
diff --git a/packages/dashboard-v2/src/pages/auth/login.js b/packages/dashboard-v2/src/pages/auth/login.js
new file mode 100644
index 00000000..077dda61
--- /dev/null
+++ b/packages/dashboard-v2/src/pages/auth/login.js
@@ -0,0 +1,23 @@
+import { navigate } from "gatsby";
+
+import AuthLayout from "../../layouts/AuthLayout";
+
+import { LoginForm } from "../../components/forms";
+
+const LoginPage = ({ location }) => {
+ const query = new URLSearchParams(location.search);
+ const redirectTo = query.get("return_to");
+
+ return (
+
+
+
+
+
navigate(redirectTo || "/")} />
+
+ );
+};
+
+LoginPage.Layout = AuthLayout;
+
+export default LoginPage;
diff --git a/packages/dashboard-v2/src/services/accountsService.js b/packages/dashboard-v2/src/services/accountsService.js
new file mode 100644
index 00000000..37244e5f
--- /dev/null
+++ b/packages/dashboard-v2/src/services/accountsService.js
@@ -0,0 +1,3 @@
+import ky from "ky";
+
+export default ky.create({ prefixUrl: "/api" });
diff --git a/packages/dashboard-v2/static/images/auth-bg.svg b/packages/dashboard-v2/static/images/auth-bg.svg
new file mode 100644
index 00000000..89d598f8
--- /dev/null
+++ b/packages/dashboard-v2/static/images/auth-bg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/dashboard-v2/static/images/logo-black-text.svg b/packages/dashboard-v2/static/images/logo-black-text.svg
new file mode 100644
index 00000000..40e45ba8
--- /dev/null
+++ b/packages/dashboard-v2/static/images/logo-black-text.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/dashboard-v2/yarn.lock b/packages/dashboard-v2/yarn.lock
index 12ff2eb1..687e3c5b 100644
--- a/packages/dashboard-v2/yarn.lock
+++ b/packages/dashboard-v2/yarn.lock
@@ -3373,6 +3373,11 @@
dependencies:
"@types/node" "*"
+"@types/lodash@^4.14.175":
+ version "4.14.180"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
+ integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g==
+
"@types/lodash@^4.14.92":
version "4.14.178"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"
@@ -6366,6 +6371,11 @@ deep-object-diff@^1.1.0:
resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.7.tgz#348b3246f426427dd633eaa50e1ed1fc2eafc7e4"
integrity sha512-QkgBca0mL08P6HiOjoqvmm6xOAl2W6CT2+34Ljhg0OeFan8cwlcdq8jrLKsBBuUFAZLsN5b6y491KdKEoSo9lg==
+deepmerge@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
+ integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
+
deepmerge@^4.0.0, deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
@@ -7951,6 +7961,19 @@ format@^0.2.0:
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=
+formik@^2.2.9:
+ version "2.2.9"
+ resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0"
+ integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==
+ dependencies:
+ deepmerge "^2.1.1"
+ hoist-non-react-statics "^3.3.0"
+ lodash "^4.17.21"
+ lodash-es "^4.17.21"
+ react-fast-compare "^2.0.1"
+ tiny-warning "^1.0.2"
+ tslib "^1.10.0"
+
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@@ -10357,6 +10380,11 @@ klona@^2.0.4:
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
+ky@^0.30.0:
+ version "0.30.0"
+ resolved "https://registry.yarnpkg.com/ky/-/ky-0.30.0.tgz#a3d293e4f6c4604a9a4694eceb6ce30e73d27d64"
+ integrity sha512-X/u76z4JtDVq10u1JA5UQfatPxgPaVDMYTrgHyiTpGN2z4TMEJkIHsoSBBSg9SWZEIXTKsi9kHgiQ9o3Y/4yog==
+
language-subtag-registry@~0.3.2:
version "0.3.21"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
@@ -10510,6 +10538,11 @@ lock@^1.1.0:
resolved "https://registry.yarnpkg.com/lock/-/lock-1.1.0.tgz#53157499d1653b136ca66451071fca615703fa55"
integrity sha1-UxV0mdFlOxNspmRRBx/KYVcD+lU=
+lodash-es@^4.17.21:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+ integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
lodash.clonedeep@4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
@@ -11183,6 +11216,11 @@ nano-css@^5.3.1:
stacktrace-js "^2.0.2"
stylis "^4.0.6"
+nanoclone@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
+ integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
+
nanoid@^3.1.23, nanoid@^3.2.0, nanoid@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
@@ -12647,6 +12685,11 @@ proper-lockfile@^4.1.2:
retry "^0.12.0"
signal-exit "^3.0.2"
+property-expr@^2.0.4:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4"
+ integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==
+
property-information@^5.0.0, property-information@^5.3.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
@@ -12942,6 +12985,11 @@ react-error-overlay@^6.0.9:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
+react-fast-compare@^2.0.1:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
+ integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
+
react-fast-compare@^3.0.1, react-fast-compare@^3.1.1, react-fast-compare@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
@@ -14830,6 +14878,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+tiny-warning@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+ integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
tinycolor2@^1.4.1:
version "1.4.2"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
@@ -14929,6 +14982,11 @@ token-types@^4.1.1:
"@tokenizer/token" "^0.3.0"
ieee754 "^1.2.1"
+toposort@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
+ integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
+
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
@@ -16036,6 +16094,19 @@ yoga-layout-prebuilt@^1.10.0:
dependencies:
"@types/yoga-layout" "1.9.2"
+yup@^0.32.11:
+ version "0.32.11"
+ resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5"
+ integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==
+ dependencies:
+ "@babel/runtime" "^7.15.4"
+ "@types/lodash" "^4.14.175"
+ lodash "^4.17.21"
+ lodash-es "^4.17.21"
+ nanoclone "^0.2.1"
+ property-expr "^2.0.4"
+ toposort "^2.0.2"
+
yurnalist@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/yurnalist/-/yurnalist-2.1.0.tgz#44cf7ea5a33a8fab4968cc8c2970489f93760902"