diff --git a/packages/dashboard-v2/gatsby-config.js b/packages/dashboard-v2/gatsby-config.js
index 017a4dfc..160a7784 100644
--- a/packages/dashboard-v2/gatsby-config.js
+++ b/packages/dashboard-v2/gatsby-config.js
@@ -26,7 +26,7 @@ module.exports = {
app.use(
"/api/",
createProxyMiddleware({
- target: "https://account.siasky.net",
+ target: "https://account.skynetpro.net",
secure: false, // Do not reject self-signed certificates.
changeOrigin: true,
})
diff --git a/packages/dashboard-v2/src/components/forms/LoginForm.js b/packages/dashboard-v2/src/components/forms/LoginForm.js
index 61973215..a5a1deeb 100644
--- a/packages/dashboard-v2/src/components/forms/LoginForm.js
+++ b/packages/dashboard-v2/src/components/forms/LoginForm.js
@@ -70,7 +70,7 @@ export const LoginForm = ({ onSuccess }) => {
touched={touched.password}
/>
-
+
Forgot your password?
diff --git a/packages/dashboard-v2/src/components/forms/RecoveryForm.js b/packages/dashboard-v2/src/components/forms/RecoveryForm.js
new file mode 100644
index 00000000..50564b34
--- /dev/null
+++ b/packages/dashboard-v2/src/components/forms/RecoveryForm.js
@@ -0,0 +1,57 @@
+import PropTypes from "prop-types";
+import { Formik, Form } from "formik";
+import * as Yup from "yup";
+
+import { TextField } from "../Form/TextField";
+import { Button } from "../Button";
+
+import accountsService from "../../services/accountsService";
+
+const recoverySchema = Yup.object().shape({
+ email: Yup.string().required("Email is required").email("Please provide a valid email address"),
+});
+
+export const RecoveryForm = ({ onSuccess, onFailure }) => (
+ {
+ try {
+ await accountsService.post("user/recover/request", {
+ json: values,
+ });
+
+ onSuccess();
+ } catch {
+ onFailure();
+ }
+ }}
+ >
+ {({ errors, touched }) => (
+
+ )}
+
+);
+
+RecoveryForm.propTypes = {
+ onFailure: PropTypes.func.isRequired,
+ onSuccess: PropTypes.func.isRequired,
+};
diff --git a/packages/dashboard-v2/src/components/forms/ResetPasswordForm.js b/packages/dashboard-v2/src/components/forms/ResetPasswordForm.js
new file mode 100644
index 00000000..68e8480f
--- /dev/null
+++ b/packages/dashboard-v2/src/components/forms/ResetPasswordForm.js
@@ -0,0 +1,67 @@
+import PropTypes from "prop-types";
+import { Formik, Form } from "formik";
+
+import { TextField } from "../Form/TextField";
+import { Button } from "../Button";
+import { passwordSchema } from "./SignUpForm";
+
+import accountsService from "../../services/accountsService";
+
+export const ResetPasswordForm = ({ token, onSuccess, onFailure }) => (
+ {
+ try {
+ await accountsService.post("user/recover", {
+ json: {
+ token,
+ password,
+ confirmPassword,
+ },
+ });
+
+ onSuccess();
+ } catch {
+ onFailure();
+ }
+ }}
+ >
+ {({ errors, touched }) => (
+
+ )}
+
+);
+
+ResetPasswordForm.propTypes = {
+ token: PropTypes.string.isRequired,
+ onFailure: PropTypes.func.isRequired,
+ onSuccess: PropTypes.func.isRequired,
+};
diff --git a/packages/dashboard-v2/src/components/forms/SignUpForm.js b/packages/dashboard-v2/src/components/forms/SignUpForm.js
new file mode 100644
index 00000000..976c0c74
--- /dev/null
+++ b/packages/dashboard-v2/src/components/forms/SignUpForm.js
@@ -0,0 +1,109 @@
+import PropTypes from "prop-types";
+import { Formik, Form } from "formik";
+import * as Yup from "yup";
+
+import { TextField } from "../Form/TextField";
+import { Button } from "../Button";
+
+import accountsService from "../../services/accountsService";
+
+export const passwordSchema = Yup.object().shape({
+ password: Yup.string().required("Password is required"),
+ confirmPassword: Yup.string()
+ .oneOf([Yup.ref("password"), null], "Passwords must match")
+ .required("Please re-type your password"),
+});
+
+const registrationSchema = Yup.object()
+ .shape({
+ email: Yup.string().required("Email is required").email("Please provide a valid email address"),
+ })
+ .concat(passwordSchema);
+
+const USER_EXISTS_ERROR = "identity already belongs to an existing user";
+
+export const SignUpForm = ({ onSuccess, onFailure }) => (
+ {
+ try {
+ await accountsService.post("user", {
+ json: {
+ email,
+ password,
+ },
+ });
+
+ onSuccess();
+ } catch (err) {
+ let isFormErrorSet = false;
+
+ if (err.response) {
+ const data = await err.response.json();
+
+ // If it's a user error, let's capture it and handle within the form.
+ if (USER_EXISTS_ERROR === data.message) {
+ setErrors({ email: "This email address already in use." });
+ isFormErrorSet = true;
+ }
+ }
+
+ if (!isFormErrorSet) {
+ onFailure();
+ }
+ }
+ }}
+ >
+ {({ errors, touched }) => (
+
+ )}
+
+);
+
+SignUpForm.propTypes = {
+ onFailure: PropTypes.func.isRequired,
+ onSuccess: PropTypes.func.isRequired,
+};
diff --git a/packages/dashboard-v2/src/layouts/AuthLayout.js b/packages/dashboard-v2/src/layouts/AuthLayout.js
index 9706c83b..85604321 100644
--- a/packages/dashboard-v2/src/layouts/AuthLayout.js
+++ b/packages/dashboard-v2/src/layouts/AuthLayout.js
@@ -3,10 +3,10 @@ import styled from "styled-components";
import { SWRConfig } from "swr";
import { UserProvider } from "../contexts/user";
-import { guestsOnly } from "../lib/swrConfig";
+import { guestsOnly, allUsers } from "../lib/swrConfig";
const Layout = styled.div.attrs({
- className: "h-screen w-screen bg-black flex",
+ className: "min-h-screen w-screen bg-black flex",
})`
background-image: url(/images/auth-bg.svg);
background-repeat: no-repeat;
@@ -21,25 +21,30 @@ 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}
-
-
-
- >
- );
-};
+const AuthLayout =
+ (swrConfig) =>
+ ({ children }) => {
+ return (
+ <>
+
+
+
+
+
+
+ The decentralized revolution starts with decentralized storage
+
+
+
+ {children}
+
+
+
+ >
+ );
+ };
-export default AuthLayout;
+// Some pages (e.g. email confirmation) need to be accessible to both logged-in and guest users.
+export const AllUsersAuthLayout = AuthLayout(allUsers);
+
+export default AuthLayout(guestsOnly);
diff --git a/packages/dashboard-v2/src/lib/swrConfig.js b/packages/dashboard-v2/src/lib/swrConfig.js
index 3e5f730a..058b5ead 100644
--- a/packages/dashboard-v2/src/lib/swrConfig.js
+++ b/packages/dashboard-v2/src/lib/swrConfig.js
@@ -23,6 +23,10 @@ const redirectAuthenticated = (key) =>
return response.json();
});
+export const allUsers = {
+ fetcher: (key) => fetch(`${baseUrl}/${key}`).then((response) => response.json()),
+};
+
export const authenticatedOnly = {
fetcher: redirectUnauthenticated,
};
diff --git a/packages/dashboard-v2/src/pages/auth/login.js b/packages/dashboard-v2/src/pages/auth/login.js
index 077dda61..e3b5240b 100644
--- a/packages/dashboard-v2/src/pages/auth/login.js
+++ b/packages/dashboard-v2/src/pages/auth/login.js
@@ -9,8 +9,8 @@ const LoginPage = ({ location }) => {
const redirectTo = query.get("return_to");
return (
-
-
+
+
navigate(redirectTo || "/")} />
diff --git a/packages/dashboard-v2/src/pages/auth/reset-password.js b/packages/dashboard-v2/src/pages/auth/reset-password.js
new file mode 100644
index 00000000..8ecbfeaa
--- /dev/null
+++ b/packages/dashboard-v2/src/pages/auth/reset-password.js
@@ -0,0 +1,48 @@
+import { useState } from "react";
+
+import AuthLayout from "../../layouts/AuthLayout";
+
+import { RecoveryForm } from "../../components/forms/RecoveryForm";
+import HighlightedLink from "../../components/HighlightedLink";
+
+const State = {
+ Pure: "PURE",
+ Success: "SUCCESS",
+ Failure: "FAILURE",
+};
+
+const ResetPasswordPage = () => {
+ const [state, setState] = useState(State.Pure);
+
+ return (
+
+
+
+
+ {state !== State.Success && (
+
setState(State.Success)} onFailure={() => setState(State.Failure)} />
+ )}
+
+ {state === State.Success && (
+ Please check your inbox for further instructions.
+ )}
+
+ {state === State.Failure && (
+ Something went wrong, please try again later.
+ )}
+
+
+
+ Suddenly remembered your password? Sign in
+
+
+ Don't actually have an account? Create one!
+
+
+
+ );
+};
+
+ResetPasswordPage.Layout = AuthLayout;
+
+export default ResetPasswordPage;
diff --git a/packages/dashboard-v2/src/pages/auth/signup.js b/packages/dashboard-v2/src/pages/auth/signup.js
new file mode 100644
index 00000000..4acbae85
--- /dev/null
+++ b/packages/dashboard-v2/src/pages/auth/signup.js
@@ -0,0 +1,56 @@
+import { useEffect, useState } from "react";
+
+import AuthLayout from "../../layouts/AuthLayout";
+
+import HighlightedLink from "../../components/HighlightedLink";
+import { SignUpForm } from "../../components/forms/SignUpForm";
+import { navigate } from "gatsby";
+
+const State = {
+ Pure: "PURE",
+ Success: "SUCCESS",
+ Failure: "FAILURE",
+};
+
+const SignUpPage = () => {
+ const [state, setState] = useState(State.Pure);
+
+ useEffect(() => {
+ if (state === State.Success) {
+ const timer = setTimeout(() => navigate("/"), 3000);
+
+ return () => clearTimeout(timer);
+ }
+ }, [state]);
+
+ return (
+
+
+
+
+ {state !== State.Success && (
+
setState(State.Success)} onFailure={() => setState(State.Failure)} />
+ )}
+
+ {state === State.Success && (
+
+
Please check your inbox and confirm your email address.
+
You will be redirected to your dashboard shortly.
+
Click here to go there now.
+
+ )}
+
+ {state === State.Failure && (
+ Something went wrong, please try again later.
+ )}
+
+
+ Already have an account? Sign in
+
+
+ );
+};
+
+SignUpPage.Layout = AuthLayout;
+
+export default SignUpPage;
diff --git a/packages/dashboard-v2/src/pages/user/confirm.js b/packages/dashboard-v2/src/pages/user/confirm.js
new file mode 100644
index 00000000..9e95e3e3
--- /dev/null
+++ b/packages/dashboard-v2/src/pages/user/confirm.js
@@ -0,0 +1,78 @@
+import { useEffect, useState } from "react";
+import { navigate } from "gatsby";
+
+import { AllUsersAuthLayout } from "../../layouts/AuthLayout";
+
+import HighlightedLink from "../../components/HighlightedLink";
+import accountsService from "../../services/accountsService";
+
+const State = {
+ Pure: "PURE",
+ Success: "SUCCESS",
+ Failure: "FAILURE",
+};
+
+const EmailConfirmationPage = ({ location }) => {
+ const query = new URLSearchParams(location.search);
+ const token = query.get("token");
+
+ const [state, setState] = useState(State.Pure);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ let timer;
+
+ async function confirm(token) {
+ try {
+ await accountsService.get("user/confirm", {
+ signal: controller.signal,
+ searchParams: { token },
+ });
+
+ timer = setTimeout(() => {
+ navigate("/");
+ }, 3000);
+ setState(State.Success);
+ } catch (err) {
+ // Don't show an error message if request was aborted due to `token` changing.
+ if (err.code !== DOMException.ABORT_ERR) {
+ setState(State.Failure);
+ }
+ }
+ }
+
+ if (token) {
+ confirm(token);
+ }
+
+ return () => {
+ controller.abort();
+ clearTimeout(timer);
+ };
+ }, [token]);
+
+ return (
+
+
+
+
+
+ {state === State.Pure &&
Please wait while we verify your account...
}
+
+ {state === State.Success && (
+ <>
+
All done!
+
You will be redirected to your dashboard shortly.
+
Redirect now.
+ >
+ )}
+
+ {state === State.Failure &&
Something went wrong, please try again later.
}
+
+
+ );
+};
+
+EmailConfirmationPage.Layout = AllUsersAuthLayout;
+
+export default EmailConfirmationPage;
diff --git a/packages/dashboard-v2/src/pages/user/recover.js b/packages/dashboard-v2/src/pages/user/recover.js
new file mode 100644
index 00000000..686e677a
--- /dev/null
+++ b/packages/dashboard-v2/src/pages/user/recover.js
@@ -0,0 +1,56 @@
+import { useState } from "react";
+import { navigate } from "gatsby";
+
+import AuthLayout from "../../layouts/AuthLayout";
+
+import { ResetPasswordForm } from "../../components/forms/ResetPasswordForm";
+import HighlightedLink from "../../components/HighlightedLink";
+
+const State = {
+ Pure: "PURE",
+ Success: "SUCCESS",
+ Failure: "FAILURE",
+};
+
+const RecoverPage = ({ location }) => {
+ const query = new URLSearchParams(location.search);
+ const token = query.get("token");
+
+ const [state, setState] = useState(State.Pure);
+
+ return (
+
+
+
+
+ {state !== State.Success && (
+
{
+ setState(State.Success);
+ navigate("/");
+ }}
+ onFailure={() => setState(State.Failure)}
+ />
+ )}
+
+ {state === State.Success && (
+
+ All done! You will be redirected to your dashboard shortly.
+
+ )}
+
+ {state === State.Failure && (
+ Something went wrong, please try again later.
+ )}
+
+
+ Suddenly remembered your old password? Sign in
+
+
+ );
+};
+
+RecoverPage.Layout = AuthLayout;
+
+export default RecoverPage;