From 3be1718022b5921b0f83c41fbab0c3b594d7ab21 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 8 Jan 2024 22:21:35 +0100 Subject: [PATCH] password reset implemented --- package-lock.json | 25 ++++++ package.json | 1 + src/Components/AppShell/AppShell.tsx | 50 ++++++----- src/Components/AppShell/Content.tsx | 2 +- src/Components/AppShell/NavBar.tsx | 86 ++---------------- src/Components/Auth/LoginPage.tsx | 99 +++++++++------------ src/Components/Auth/RequestPasswordPage.tsx | 48 ++++++++++ src/Components/Auth/SetNewPasswordPage.tsx | 50 +++++++++++ src/Components/Auth/SignupPage.tsx | 94 +++++++++---------- src/Components/Auth/index.tsx | 4 +- src/Components/Auth/useAuth.tsx | 34 ++++++- src/Components/Map/UtopiaMap.tsx | 4 + src/Components/Profile/ColorPicker.tsx | 2 +- src/Components/Profile/ProfileSettings.tsx | 6 +- src/Components/Templates/MapOverlayPage.tsx | 30 +++++++ src/Components/Templates/index.tsx | 1 + src/index.tsx | 2 +- src/types.ts | 4 +- 18 files changed, 323 insertions(+), 219 deletions(-) create mode 100644 src/Components/Auth/RequestPasswordPage.tsx create mode 100644 src/Components/Auth/SetNewPasswordPage.tsx create mode 100644 src/Components/Templates/MapOverlayPage.tsx diff --git a/package-lock.json b/package-lock.json index 5f65aea4..397cf321 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@heroicons/react": "^2.0.17", + "@tanstack/react-query": "^5.17.8", "@types/offscreencanvas": "^2019.7.1", "leaflet": "^1.9.4", "prop-types": "^15.8.1", @@ -256,6 +257,30 @@ "node": ">=14.0.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.17.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.17.8.tgz", + "integrity": "sha512-V4hQv4jmRwbji9wo3F6/JQEjbWLUlv2sE2K5R49girEyok71ksDnVHbQiAvp4+FbovMY8A3IbP3cH3jqAUe/BQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.17.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.17.8.tgz", + "integrity": "sha512-J+QMBoQiAZglwVB/bEgmf9IczLPg0YwFaqmn4aTW72ZYkUYyBU9dj6OMQk3RzmD9RzUz2m7pPBCkcT0Z1i0acg==", + "dependencies": { + "@tanstack/query-core": "5.17.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", diff --git a/package.json b/package.json index c8611279..3278e600 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ }, "dependencies": { "@heroicons/react": "^2.0.17", + "@tanstack/react-query": "^5.17.8", "@types/offscreencanvas": "^2019.7.1", "leaflet": "^1.9.4", "prop-types": "^15.8.1", diff --git a/src/Components/AppShell/AppShell.tsx b/src/Components/AppShell/AppShell.tsx index 13d8124d..e9f78914 100644 --- a/src/Components/AppShell/AppShell.tsx +++ b/src/Components/AppShell/AppShell.tsx @@ -6,31 +6,37 @@ import { QuestsProvider } from '../Gaming/hooks/useQuests' import { AssetsProvider, useSetAssetApi } from './hooks/useAssets' import { SetAssetsApi } from './SetAssetsApi' import { AssetsApi } from '../../types' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -export function AppShell({ appName, nameWidth, children, assetsApi } : {appName: string, nameWidth?: number, children: React.ReactNode, assetsApi: AssetsApi}) { +export function AppShell({ appName, nameWidth, children, assetsApi }: { appName: string, nameWidth?: number, children: React.ReactNode, assetsApi: AssetsApi }) { + + // Create a client + const queryClient = new QueryClient() return ( - - - - - - -
- {children} -
-
-
-
+ + + + + + + +
+ {children} +
+
+
+
+
) } diff --git a/src/Components/AppShell/Content.tsx b/src/Components/AppShell/Content.tsx index 31db7f6b..4c463756 100644 --- a/src/Components/AppShell/Content.tsx +++ b/src/Components/AppShell/Content.tsx @@ -6,7 +6,7 @@ type ContentProps = { export function Content({children} : ContentProps) { return ( -
+
{children}
diff --git a/src/Components/AppShell/NavBar.tsx b/src/Components/AppShell/NavBar.tsx index 36699ebb..11978f15 100644 --- a/src/Components/AppShell/NavBar.tsx +++ b/src/Components/AppShell/NavBar.tsx @@ -10,63 +10,11 @@ import DialogModal from "../Templates/DialogModal"; export default function NavBar({ appName, nameWidth = 200}: { appName: string, nameWidth?: number }) { - - const [signupOpen, setSignupOpen] = useState(false); - const [loginOpen, setLoginOpen] = useState(false); - - - const [email, setEmail] = useState(""); - const [userName, setUserName] = useState(""); - const [password, setPassword] = useState(""); - - - const { isAuthenticated, user, login, register, loading, logout, token } = useAuth(); - const navigate = useNavigate(); - const onRegister = async () => { - await toast.promise( - register({ email: email, password: password }, userName), - { - success: { - render({ data }) { - navigate(`/`); - return `Hi ${data?.first_name}` - }, - // other options - icon: "✌️", - }, - error: { - render( {data} ) { - return `${data}` - }, - }, - pending: 'creating new user ...' - }); - setSignupOpen(false); - } - const onLogin = async () => { - await toast.promise( - login({ email: email, password: password }), - { - success: { - render({ data }) { - navigate(`/`); - return `Hi ${data?.first_name}` - }, - // other options - icon: "✌️", - }, - error: { - render( {data} ) { - return `${data}` - }, - }, - pending: 'logging in ...' - }); - setLoginOpen(false); - } + + const { isAuthenticated, user, logout, token } = useAuth(); const onLogout = () => { toast.promise( @@ -133,11 +81,11 @@ export default function NavBar({ appName, nameWidth = 200}: { appName: string, n
-
setLoginOpen(true)} className="tw-btn tw-btn-ghost tw-mr-2"> +
navigate("/login")} className="tw-btn tw-btn-ghost tw-mr-2"> Login
-
setSignupOpen(true)} className="tw-btn tw-btn-ghost tw-mr-2"> +
navigate("/signup")} className="tw-btn tw-btn-ghost tw-mr-2"> Sign Up
@@ -150,35 +98,15 @@ export default function NavBar({ appName, nameWidth = 200}: { appName: string, n
}
- setLoginOpen(false)}> - setEmail(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> - setPassword(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> -
- -
-
- setSignupOpen(false)}> - setUserName(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> - setEmail(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> - setPassword(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> -
- -
-
+ ) } diff --git a/src/Components/Auth/LoginPage.tsx b/src/Components/Auth/LoginPage.tsx index a26a78ab..b2c90a85 100644 --- a/src/Components/Auth/LoginPage.tsx +++ b/src/Components/Auth/LoginPage.tsx @@ -1,66 +1,53 @@ -import {useState} from 'react' -import {Link} from 'react-router-dom' +import { useRef, useState } from 'react' +import { Link, useNavigate } from 'react-router-dom' import ErrorText from '../Typography/ErrorText' -import {TextInput} from '../Input/TextInput' +import { TextInput } from '../Input/TextInput' import * as React from 'react' +import { toast } from 'react-toastify' +import { useAuth } from './useAuth' +import { MapOverlayPage} from '../Templates' -export function LoginPage(){ +export function LoginPage() { - const INITIAL_LOGIN_OBJ = { - password : "", - emailId : "" + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const { login, loading } = useAuth(); + + const navigate = useNavigate(); + + const onLogin = async () => { + await toast.promise( + login({ email: email, password: password }), + { + success: { + render({ data }) { + navigate(`/`); + return `Hi ${data?.first_name}` + }, + // other options + icon: "✌️", + }, + error: { + render({ data }) { + return `${data}` + }, + }, + pending: 'logging in ...' + }); } - const [loading, setLoading] = useState(false) - const [errorMessage, setErrorMessage] = useState("") - const [loginObj, setLoginObj] = useState(INITIAL_LOGIN_OBJ) - - const submitForm = (e) =>{ - e.preventDefault() - setErrorMessage("") - - if(loginObj.emailId.trim() === "")return setErrorMessage("Email Id is required! (use any value)") - if(loginObj.password.trim() === "")return setErrorMessage("Password is required! (use any value)") - else{ - setLoading(true) - // Call API to check user credentials and save token in localstorage - localStorage.setItem("token", "DumyTokenHere") - setLoading(false) - window.location.href = '/app/welcome' - } - } - - const updateFormValue = (val: string) => { - console.log(val) - } - - return( -
-
-
-
-

Login

-
submitForm(e)}> - -
- - updateFormValue(v)}/> - - updateFormValue(v)}/> - + return ( + +

Login

+ setEmail(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> + setPassword(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> +
Forgot Password?
- -
Forgot Password? -
- - {errorMessage} - - -
Don't have an account yet? Sign Up
- -
+
+
-
-
+ ) } + diff --git a/src/Components/Auth/RequestPasswordPage.tsx b/src/Components/Auth/RequestPasswordPage.tsx new file mode 100644 index 00000000..d297f8a8 --- /dev/null +++ b/src/Components/Auth/RequestPasswordPage.tsx @@ -0,0 +1,48 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' + +import * as React from 'react' +import { toast } from 'react-toastify' +import { useAuth } from './useAuth' +import { MapOverlayPage} from '../Templates' + +export function RequestPasswordPage({reset_url}) { + + const [email, setEmail] = useState(""); + + const { requestPasswordReset, loading } = useAuth(); + + const navigate = useNavigate(); + + const onReset = async () => { + await toast.promise( + requestPasswordReset( email, reset_url), + { + success: { + render() { + navigate(`/`); + return `Check your mailbox` + }, + // other options + icon: "📬", + }, + error: { + render({ data }) { + return `${data}` + }, + }, + pending: 'sending email ...' + }); + } + + return ( + +

Reset Password

+ setEmail(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> +
+ +
+
+ ) +} + diff --git a/src/Components/Auth/SetNewPasswordPage.tsx b/src/Components/Auth/SetNewPasswordPage.tsx new file mode 100644 index 00000000..676404db --- /dev/null +++ b/src/Components/Auth/SetNewPasswordPage.tsx @@ -0,0 +1,50 @@ +import { useRef, useState } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import ErrorText from '../Typography/ErrorText' +import { TextInput } from '../Input/TextInput' +import * as React from 'react' +import { toast } from 'react-toastify' +import { useAuth } from './useAuth' +import { MapOverlayPage} from '../Templates' + +export function SetNewPasswordPage() { + + const [password, setPassword] = useState(""); + + const { passwordReset, loading } = useAuth(); + + const navigate = useNavigate(); + + const onReset = async () => { + const token = window.location.search.split("token=")[1]; + console.log(token); + + await toast.promise( + passwordReset(token, password), + { + success: { + render() { + navigate(`/`); + return `New password set` + }, + }, + error: { + render({ data }) { + return `${data}` + }, + }, + pending: 'setting password ...' + }); + } + + return ( + +

Set new Password

+ setPassword(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> +
+ +
+
+ ) +} + diff --git a/src/Components/Auth/SignupPage.tsx b/src/Components/Auth/SignupPage.tsx index 38879502..64b54e78 100644 --- a/src/Components/Auth/SignupPage.tsx +++ b/src/Components/Auth/SignupPage.tsx @@ -1,63 +1,55 @@ -import { useState } from 'react' -import { Link } from 'react-router-dom' +import { useRef, useState } from 'react' +import { Link, useNavigate } from 'react-router-dom' import ErrorText from '../Typography/ErrorText' -import {TextInput} from '../Input/TextInput' +import { TextInput } from '../Input/TextInput' import * as React from 'react' +import { toast } from 'react-toastify' +import { useAuth } from './useAuth' +import { MapOverlayPage } from '../Templates' export function SignupPage() { - const INITIAL_REGISTER_OBJ = { - name : "", - password : "", - emailId : "" + const [email, setEmail] = useState(""); + const [userName, setUserName] = useState(""); + + const [password, setPassword] = useState(""); + + const { register, loading } = useAuth(); + + const navigate = useNavigate(); + + const onRegister = async () => { + await toast.promise( + register({ email: email, password: password }, userName), + { + success: { + render({ data }) { + navigate(`/`); + return `Hi ${data?.first_name}` + }, + // other options + icon: "✌️", + }, + error: { + render({ data }) { + return `${data}` + }, + }, + pending: 'creating new user ...' + }); } - const [loading, setLoading] = useState(false) - const [errorMessage, setErrorMessage] = useState("") - const [registerObj, setRegisterObj] = useState(INITIAL_REGISTER_OBJ) - - const submitForm = (e) =>{ - e.preventDefault() - setErrorMessage("") - - if(registerObj.name.trim() === "")return setErrorMessage("Name is required! (use any value)") - if(registerObj.emailId.trim() === "")return setErrorMessage("Email Id is required! (use any value)") - if(registerObj.password.trim() === "")return setErrorMessage("Password is required! (use any value)") - else{ - setLoading(true) - // Call API to check user credentials and save token in localstorage - localStorage.setItem("token", "DumyTokenHere") - setLoading(false) - window.location.href = '/app/welcome' - } - } - - const updateFormValue = (val: string) => { - console.log(val) - } return ( -
-
-
-
-

Sign Up

-
submitForm(e)}> - -
- - - -
- - {errorMessage} - - -
Already have an account? Login
-
-
-
+ +

Sign Up

+ setUserName(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> + setEmail(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> + setPassword(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" /> +
+
-
+ ) } + diff --git a/src/Components/Auth/index.tsx b/src/Components/Auth/index.tsx index 0142c432..1e3aee63 100644 --- a/src/Components/Auth/index.tsx +++ b/src/Components/Auth/index.tsx @@ -1,3 +1,5 @@ export {AuthProvider, useAuth} from "./useAuth" export {LoginPage} from "./LoginPage" -export {SignupPage} from "./SignupPage" \ No newline at end of file +export {SignupPage} from './SignupPage' +export {RequestPasswordPage} from './RequestPasswordPage' +export {SetNewPasswordPage} from './SetNewPasswordPage' \ No newline at end of file diff --git a/src/Components/Auth/useAuth.tsx b/src/Components/Auth/useAuth.tsx index 8b1c924f..043b3e06 100644 --- a/src/Components/Auth/useAuth.tsx +++ b/src/Components/Auth/useAuth.tsx @@ -24,7 +24,9 @@ type AuthContextProps = { loading: Boolean, logout: () => Promise, updateUser: (user: UserItem) => any, - token: String | null + token: String | null, + requestPasswordReset: (email:string, reset_url: string) => Promise, + passwordReset: (token:string, new_password:string) => Promise } const AuthContext = createContext({ @@ -35,7 +37,9 @@ const AuthContext = createContext({ loading: false, logout: () => Promise.reject(), updateUser: () => Promise.reject(), - token: "" + token: "", + requestPasswordReset: () => Promise.reject(), + passwordReset: () => Promise.reject() }); export const AuthProvider = ({ userApi, children }: AuthProviderProps) => { @@ -118,10 +122,34 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => { }; } + const requestPasswordReset = async (email: string, reset_url?: string): Promise => { + setLoading(true); + try { + await userApi.requestPasswordReset(email, reset_url); + return setLoading(false); + } catch (error: any) { + setLoading(false); + throw error; + }; + } + + + const passwordReset = async (token: string, new_password:string): Promise => { + setLoading(true); + try { + await userApi.passwordReset(token, new_password); + return setLoading(false); + } catch (error: any) { + setLoading(false); + throw error; + }; + } + + return ( {children} diff --git a/src/Components/Map/UtopiaMap.tsx b/src/Components/Map/UtopiaMap.tsx index 7012bb52..7c1f440e 100644 --- a/src/Components/Map/UtopiaMap.tsx +++ b/src/Components/Map/UtopiaMap.tsx @@ -18,6 +18,7 @@ import { LeafletRefsProvider } from "./hooks/useLeafletRefs"; import { LayerControl } from "./Subcomponents/LayerControl"; import { QuestControl } from "./Subcomponents/QuestControl"; import { Control } from "./Subcomponents/Control"; +import { Outlet } from "react-router-dom"; export interface MapEventListenerProps { @@ -71,6 +72,7 @@ function UtopiaMap({ return ( + <> @@ -114,6 +116,8 @@ function UtopiaMap({ + + ); } diff --git a/src/Components/Profile/ColorPicker.tsx b/src/Components/Profile/ColorPicker.tsx index 49a941ab..d18afb5e 100644 --- a/src/Components/Profile/ColorPicker.tsx +++ b/src/Components/Profile/ColorPicker.tsx @@ -4,7 +4,7 @@ import { HexColorPicker } from "react-colorful"; import "./ColorPicker.css" import useClickOutside from "./useClickOutside"; -export const ColorPicker = ({ color, onChange, className }) => { +export const ColorPicker = ({ color = "#000", onChange, className }) => { const popover = useRef(null); const [isOpen, toggle] = useState(false); diff --git a/src/Components/Profile/ProfileSettings.tsx b/src/Components/Profile/ProfileSettings.tsx index 5ac14884..ba729d08 100644 --- a/src/Components/Profile/ProfileSettings.tsx +++ b/src/Components/Profile/ProfileSettings.tsx @@ -158,8 +158,8 @@ export function ProfileSettings() { return ( - <> -
+
+
@@ -219,6 +219,6 @@ export function ProfileSettings() { renderCrop(); }}>Select - +
) } diff --git a/src/Components/Templates/MapOverlayPage.tsx b/src/Components/Templates/MapOverlayPage.tsx new file mode 100644 index 00000000..5b7b2e2e --- /dev/null +++ b/src/Components/Templates/MapOverlayPage.tsx @@ -0,0 +1,30 @@ + +import * as React from 'react' +import { useNavigate } from 'react-router-dom'; + +export function MapOverlayPage({children} : {children: React.ReactNode}) { + + + const closeScreen = () => { + navigate(`/`); + } + + const navigate = useNavigate(); + + + return ( +
+ +
+
+
+ {children} + +
+
+
+
+ ) +} + diff --git a/src/Components/Templates/index.tsx b/src/Components/Templates/index.tsx index 14848776..9c08cb10 100644 --- a/src/Components/Templates/index.tsx +++ b/src/Components/Templates/index.tsx @@ -1,2 +1,3 @@ export {CardPage} from './CardPage' export {TitleCard} from './TitleCard' +export {MapOverlayPage} from './MapOverlayPage' \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 651044e2..22f77e17 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,6 @@ export { UtopiaMap, Layer, Tags, Permissions, ItemForm, ItemView, PopupTextAreaInput, PopupStartEndInput, PopupTextInput, PopupButton, TextView, StartEndView } from './Components/Map'; export {AppShell, Content, SideBar} from "./Components/AppShell" -export {AuthProvider, useAuth, LoginPage, SignupPage} from "./Components/Auth" +export {AuthProvider, useAuth, LoginPage, SignupPage, RequestPasswordPage, SetNewPasswordPage} from "./Components/Auth" export {UserSettings, ProfileSettings} from './Components/Profile' export {Quests, Modal} from './Components/Gaming' export {TitleCard, CardPage} from './Components/Templates' diff --git a/src/types.ts b/src/types.ts index 8da2a863..c6af7179 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,7 +87,9 @@ export interface UserApi { logout(): Promise, getUser(): Promise, getToken(): Promise, - updateUser(user: UserItem): Promise + updateUser(user: UserItem): Promise, + requestPasswordReset(email:string, reset_url?:string), + passwordReset(token:string,new_password:string) } export type UserItem = {