From fc7053c8fc1804af47e9f72ea4ea4dd1fd7020d7 Mon Sep 17 00:00:00 2001 From: myung03 Date: Fri, 20 Sep 2024 18:16:15 -0700 Subject: [PATCH] abstracted out auth page and restyled --- .../settings/client/account/Login.tsx | 140 ------------ .../settings/client/account/Register.tsx | 178 --------------- .../settings/client/account/Tabs.tsx | 169 -------------- .../settings/client/account/index.tsx | 28 ++- interface/components/Authentication.tsx | 154 +++++++++++++ interface/components/Login.tsx | 167 ++++++++++++++ interface/components/Register.tsx | 211 ++++++++++++++++++ interface/components/ShowPassword.tsx | 27 +++ interface/components/index.ts | 1 + interface/locales/en/common.json | 6 +- packages/ui/tsconfig.json | 2 +- 11 files changed, 586 insertions(+), 497 deletions(-) delete mode 100644 interface/app/$libraryId/settings/client/account/Login.tsx delete mode 100644 interface/app/$libraryId/settings/client/account/Register.tsx delete mode 100644 interface/app/$libraryId/settings/client/account/Tabs.tsx create mode 100644 interface/components/Authentication.tsx create mode 100644 interface/components/Login.tsx create mode 100644 interface/components/Register.tsx create mode 100644 interface/components/ShowPassword.tsx diff --git a/interface/app/$libraryId/settings/client/account/Login.tsx b/interface/app/$libraryId/settings/client/account/Login.tsx deleted file mode 100644 index b64ec4b6c..000000000 --- a/interface/app/$libraryId/settings/client/account/Login.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Controller } from 'react-hook-form'; -import { signIn } from 'supertokens-web-js/recipe/emailpassword'; -import { nonLibraryClient, useZodForm } from '@sd/client'; -import { Button, Form, Input, toast, z } from '@sd/ui'; - -import ShowPassword from './ShowPassword'; - -async function signInClicked(email: string, password: string) { - try { - const response = await signIn({ - formFields: [ - { - id: 'email', - value: email - }, - { - id: 'password', - value: password - } - ] - }); - - if (response.status === 'FIELD_ERROR') { - response.formFields.forEach((formField) => { - if (formField.id === 'email') { - // Email validation failed (for example incorrect email syntax). - toast.error(formField.error); - } - }); - } else if (response.status === 'WRONG_CREDENTIALS_ERROR') { - toast.error('Email & password combination is incorrect.'); - } else if (response.status === 'SIGN_IN_NOT_ALLOWED') { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in was not allowed. - toast.error(response.reason); - } else { - // sign in successful. The session tokens are automatically handled by - // the frontend SDK. - toast.success('Sign in successful'); - // Refresh the page to reflect the new session state. - // FIXME: This is a temporary workaround. We will provide a better way to handle this. - window.location.reload(); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - toast.error(err.message); - } else { - console.error(err); - toast.error('Oops! Something went wrong.'); - } - } -} - -const LoginSchema = z.object({ - email: z.string().email(), - password: z.string().min(6) -}); - -const Login = () => { - const [showPassword, setShowPassword] = useState(false); - const form = useZodForm({ - schema: LoginSchema, - defaultValues: { - email: '', - password: '' - } - }); - - return ( -
{ - // handle login submission - await signInClicked(data.email, data.password); - })} - form={form} - > -
- ( - - )} - /> - {form.formState.errors.email && ( -

{form.formState.errors.email.message}

- )} - ( -
- { - const pastedText = e.clipboardData.getData('text'); - field.onChange(pastedText); - }} - /> - -
- )} - /> - {form.formState.errors.password && ( -

{form.formState.errors.password.message}

- )} - -
-
- ); -}; - -export default Login; diff --git a/interface/app/$libraryId/settings/client/account/Register.tsx b/interface/app/$libraryId/settings/client/account/Register.tsx deleted file mode 100644 index 6dbc4ef15..000000000 --- a/interface/app/$libraryId/settings/client/account/Register.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { useState } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { signUp } from 'supertokens-web-js/recipe/emailpassword'; -import { Button, Form, Input, toast, z } from '@sd/ui'; - -import ShowPassword from './ShowPassword'; - -const RegisterSchema = z - .object({ - email: z.string().email(), - password: z.string().min(6), - confirmPassword: z.string().min(6) - }) - .refine((data) => data.password === data.confirmPassword, { - message: 'Passwords do not match', - path: ['confirmPassword'] - }); -type RegisterType = z.infer; - -async function signUpClicked(email: string, password: string) { - try { - const response = await signUp({ - formFields: [ - { - id: 'email', - value: email - }, - { - id: 'password', - value: password - } - ] - }); - - if (response.status === 'FIELD_ERROR') { - // one of the input formFields failed validaiton - response.formFields.forEach((formField) => { - if (formField.id === 'email') { - // Email validation failed (for example incorrect email syntax), - // or the email is not unique. - toast.error(formField.error); - } else if (formField.id === 'password') { - // Password validation failed. - // Maybe it didn't match the password strength - toast.error(formField.error); - } - }); - } else if (response.status === 'SIGN_UP_NOT_ALLOWED') { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign up was not allowed. - toast.error(response.reason); - } else { - // sign up successful. The session tokens are automatically handled by - // the frontend SDK. - toast.success('Sign up successful'); - // FIXME: This is a temporary workaround. We will provide a better way to handle this. - window.location.reload(); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - toast.error(err.message); - } else { - toast.error('Oops! Something went wrong.'); - } - } -} - -const Register = () => { - const [showPassword, setShowPassword] = useState(false); - // useZodForm seems to be out-dated or needs - //fixing as it does not support the schema using zod.refine - const form = useForm({ - resolver: zodResolver(RegisterSchema), - defaultValues: { - email: '', - password: '', - confirmPassword: '' - } - }); - return ( -
{ - // handle login submission - console.log(data); - await signUpClicked(data.email, data.password); - })} - form={form} - > -
- ( - - )} - /> - {form.formState.errors.email && ( -

{form.formState.errors.email.message}

- )} - ( -
- { - const pastedText = e.clipboardData.getData('text'); - field.onChange(pastedText); - }} - /> - -
- )} - /> - {form.formState.errors.password && ( -

{form.formState.errors.password.message}

- )} - ( -
- - -
- )} - /> - {form.formState.errors.confirmPassword && ( -

- {form.formState.errors.confirmPassword.message} -

- )} - -
-
- ); -}; - -export default Register; diff --git a/interface/app/$libraryId/settings/client/account/Tabs.tsx b/interface/app/$libraryId/settings/client/account/Tabs.tsx deleted file mode 100644 index 015d67d77..000000000 --- a/interface/app/$libraryId/settings/client/account/Tabs.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { GoogleLogo, Icon } from '@phosphor-icons/react'; -import { Apple, Github } from '@sd/assets/svgs/brands'; -import { open } from '@tauri-apps/plugin-shell'; -import clsx from 'clsx'; -import { motion } from 'framer-motion'; -import { useState } from 'react'; -import { getAuthorisationURLWithQueryParamsAndSetState } from 'supertokens-web-js/recipe/thirdparty'; -import { Button, Card, Divider, toast, Tooltip } from '@sd/ui'; - -import Login from './Login'; -import Register from './Register'; - -const AccountTabs = ['Login', 'Register'] as const; - -type SocialLogin = { - name: 'Github' | 'Google' | 'Apple'; - icon: Icon; -}; - -const SocialLogins: SocialLogin[] = [ - { name: 'Github', icon: Github }, - { name: 'Google', icon: GoogleLogo }, - { name: 'Apple', icon: Apple } -]; - -const Tabs = () => { - const [activeTab, setActiveTab] = useState<'Login' | 'Register'>('Login'); - - // Currently opens in App. - const socialLoginHandlers = (name: SocialLogin['name']) => { - return { - Github: async () => { - try { - const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ - thirdPartyId: 'github', - - // This is where Github should redirect the user back after login or error. - frontendRedirectURI: 'http://localhost:9420/api/auth/callback/github' - }); - - // we redirect the user to Github for auth. - await open(authUrl); - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - toast.error(err.message); - } else { - toast.error('Oops! Something went wrong.'); - } - } - }, - Google: async () => { - try { - const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ - thirdPartyId: 'google', - - // This is where Google should redirect the user back after login or error. - // This URL goes on the Google's dashboard as well. - frontendRedirectURI: 'spacedrive://-/auth' - }); - - /* - Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow - */ - - // we redirect the user to google for auth. - await open(authUrl); - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - toast.error(err.message); - } else { - toast.error('Oops! Something went wrong.'); - } - } - }, - Apple: async () => { - try { - const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ - thirdPartyId: 'apple', - - // This is where Apple should redirect the user back after login or error. - frontendRedirectURI: 'http://localhost:9420/api/auth/callback/apple' - }); - - // we redirect the user to Apple for auth. - await open(authUrl); - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - toast.error(err.message); - } else { - toast.error('Oops! Something went wrong.'); - } - } - } - }[name](); - }; - - return ( - -
- {AccountTabs.map((text) => ( -
{ - setActiveTab(text); - }} - className={clsx( - 'relative flex-1 border-b border-app-line p-2.5 text-center', - text === 'Login' ? 'rounded-tl-md' : 'rounded-tr-md' - )} - > -

- {text} -

- {text === activeTab && ( - - )} -
- ))} -
-
- {activeTab === 'Login' ? : } - {/* Disabling for now for demo purposes. We need to figure out on the backend how the tokens are recieved so we can a) store them in the frontend and b) use them as auth tokens for our cloud services. - @Rocky43007 */} - {/*
- -

OR

- -
-
- {SocialLogins.map((social) => ( - - - - ))} -
*/} -
-
- ); -}; - -export default Tabs; diff --git a/interface/app/$libraryId/settings/client/account/index.tsx b/interface/app/$libraryId/settings/client/account/index.tsx index 63601083e..ff91e70fc 100644 --- a/interface/app/$libraryId/settings/client/account/index.tsx +++ b/interface/app/$libraryId/settings/client/account/index.tsx @@ -2,12 +2,12 @@ import { useEffect, useState } from 'react'; import Session, { signOut } from 'supertokens-web-js/recipe/session'; import { auth, useBridgeMutation, useBridgeQuery, useFeatureFlag } from '@sd/client'; import { Button, Input, toast } from '@sd/ui'; +import { Authentication } from '~/components'; import { useLocale } from '~/hooks'; import { AUTH_SERVER_URL } from '~/util'; import { Heading } from '../../Layout'; import Profile from './Profile'; -import Tabs from './Tabs'; type User = { email: string; @@ -19,6 +19,8 @@ type User = { export const Component = () => { const { t } = useLocale(); const [userInfo, setUserInfo] = useState(null); + const [reload, setReload] = useState(false); + useEffect(() => { async function _() { const user_data = await fetch(`${AUTH_SERVER_URL}/api/user`, { @@ -36,21 +38,22 @@ export const Component = () => { setUserInfo(null); } }); + setReload(false); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [reload]); return ( <> {userInfo !== null && ( -
+
+ + ))} +
*/} +
+ + ); +}; diff --git a/interface/components/Login.tsx b/interface/components/Login.tsx new file mode 100644 index 000000000..699e756f1 --- /dev/null +++ b/interface/components/Login.tsx @@ -0,0 +1,167 @@ +import clsx from 'clsx'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { Controller } from 'react-hook-form'; +import { useNavigate } from 'react-router'; +import { signIn } from 'supertokens-web-js/recipe/emailpassword'; +import { nonLibraryClient, useZodForm } from '@sd/client'; +import { Button, Form, Input, toast, z } from '@sd/ui'; +import { useIsDark, useLocale } from '~/hooks'; + +import ShowPassword from './ShowPassword'; + +async function signInClicked( + email: string, + password: string, + reload: Dispatch> +) { + try { + const response = await signIn({ + formFields: [ + { + id: 'email', + value: email + }, + { + id: 'password', + value: password + } + ] + }); + + if (response.status === 'FIELD_ERROR') { + response.formFields.forEach((formField) => { + if (formField.id === 'email') { + toast.error(formField.error); + } + }); + } else if (response.status === 'WRONG_CREDENTIALS_ERROR') { + toast.error('Email & password combination is incorrect.'); + } else if (response.status === 'SIGN_IN_NOT_ALLOWED') { + toast.error(response.reason); + } else { + toast.success('Sign in successful'); + reload(true); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + toast.error(err.message); + } else { + console.error(err); + toast.error('Oops! Something went wrong.'); + } + } +} + +const LoginSchema = z.object({ + email: z.string().email(), + password: z.string().min(6) +}); + +const Login = ({ reload }: { reload: Dispatch> }) => { + const { t } = useLocale(); + const isDark = useIsDark(); + const [showPassword, setShowPassword] = useState(false); + const navigate = useNavigate(); // useNavigate hook + const form = useZodForm({ + schema: LoginSchema, + defaultValues: { + email: '', + password: '' + } + }); + + return ( +
{ + await signInClicked(data.email, data.password, reload); + })} + form={form} + > +
+
+
+ + ( + + )} + /> + {form.formState.errors.email && ( +

+ {form.formState.errors.email.message} +

+ )} +
+ +
+ + ( +
+ { + const pastedText = e.clipboardData.getData('text'); + field.onChange(pastedText); + }} + /> + +
+ )} + /> + {form.formState.errors.password && ( +

+ {form.formState.errors.password.message} +

+ )} +
+
+ + {form.formState.errors.password && ( +

{form.formState.errors.password.message}

+ )} + +
+
+ ); +}; + +export default Login; diff --git a/interface/components/Register.tsx b/interface/components/Register.tsx new file mode 100644 index 000000000..89b4e19ab --- /dev/null +++ b/interface/components/Register.tsx @@ -0,0 +1,211 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import clsx from 'clsx'; +import { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { signUp } from 'supertokens-web-js/recipe/emailpassword'; +import { Button, Form, Input, toast, z } from '@sd/ui'; +import { useIsDark, useLocale } from '~/hooks'; + +import ShowPassword from './ShowPassword'; + +const RegisterSchema = z + .object({ + email: z.string().email(), + password: z.string().min(6), + confirmPassword: z.string().min(6) + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords do not match', + path: ['confirmPassword'] + }); +type RegisterType = z.infer; + +async function signUpClicked(email: string, password: string) { + try { + const response = await signUp({ + formFields: [ + { + id: 'email', + value: email + }, + { + id: 'password', + value: password + } + ] + }); + + if (response.status === 'FIELD_ERROR') { + // one of the input formFields failed validaiton + response.formFields.forEach((formField) => { + if (formField.id === 'email') { + // Email validation failed (for example incorrect email syntax), + // or the email is not unique. + toast.error(formField.error); + } else if (formField.id === 'password') { + // Password validation failed. + // Maybe it didn't match the password strength + toast.error(formField.error); + } + }); + } else if (response.status === 'SIGN_UP_NOT_ALLOWED') { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign up was not allowed. + toast.error(response.reason); + } else { + // sign up successful. The session tokens are automatically handled by + // the frontend SDK. + toast.success('Sign up successful'); + // FIXME: This is a temporary workaround. We will provide a better way to handle this. + window.location.reload(); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + toast.error(err.message); + } else { + toast.error('Oops! Something went wrong.'); + } + } +} + +const Register = () => { + const { t } = useLocale(); + const isDark = useIsDark(); + const [showPassword, setShowPassword] = useState(false); + // useZodForm seems to be out-dated or needs + //fixing as it does not support the schema using zod.refine + const form = useForm({ + resolver: zodResolver(RegisterSchema), + defaultValues: { + email: '', + password: '', + confirmPassword: '' + } + }); + return ( +
{ + // handle sign-up submission + console.log(data); + await signUpClicked(data.email, data.password); + })} + form={form} + > +
+
+
+ + ( + + )} + /> + {form.formState.errors.email && ( +

+ {form.formState.errors.email.message} +

+ )} +
+ +
+ + ( +
+ { + const pastedText = e.clipboardData.getData('text'); + field.onChange(pastedText); + }} + /> + +
+ )} + /> + {form.formState.errors.password && ( +

+ {form.formState.errors.password.message} +

+ )} +
+ +
+ ( +
+ + +
+ )} + /> + {form.formState.errors.confirmPassword && ( +

+ {form.formState.errors.confirmPassword.message} +

+ )} +
+
+ + +
+
+ ); +}; + +export default Register; diff --git a/interface/components/ShowPassword.tsx b/interface/components/ShowPassword.tsx new file mode 100644 index 000000000..d3e846da0 --- /dev/null +++ b/interface/components/ShowPassword.tsx @@ -0,0 +1,27 @@ +import { Eye, EyeClosed } from '@phosphor-icons/react'; +import { Button, Tooltip } from '@sd/ui'; + +interface Props { + showPassword: boolean; + setShowPassword: (value: boolean) => void; +} + +const ShowPassword = ({ showPassword, setShowPassword }: Props) => { + return ( + + + + ); +}; + +export default ShowPassword; diff --git a/interface/components/index.ts b/interface/components/index.ts index edc3d5dc5..2f6420216 100644 --- a/interface/components/index.ts +++ b/interface/components/index.ts @@ -13,3 +13,4 @@ export * from './TextViewer'; export * from './TrafficLights'; export * from './TruncatedText'; export * from './Accordion'; +export * from './Authentication'; diff --git a/interface/locales/en/common.json b/interface/locales/en/common.json index 0f97989b7..63e57208c 100644 --- a/interface/locales/en/common.json +++ b/interface/locales/en/common.json @@ -467,6 +467,7 @@ "log_out": "Log out", "logged_in_as": "Logged in as {{email}}", "logging_in": "Logging in...", + "login": "Login", "logout": "Logout", "manage_library": "Manage Library", "managed": "Managed", @@ -610,6 +611,7 @@ "regen_labels": "Regen Labels", "regen_thumbnails": "Regen Thumbnails", "regenerate_thumbs": "Regenerate Thumbs", + "register": "Register", "reindex": "Re-index", "reject": "Reject", "reject_files": "Reject files", @@ -692,9 +694,9 @@ "skip_login": "Skip login", "software": "Software", "sort_by": "Sort by", - "spacedrive_account": "Spacedrive Account", + "spacedrive_account": "Account", "spacedrive_cloud": "Spacedrive Cloud", - "spacedrive_cloud_description": "Spacedrive is always local first, but we will offer our own optional cloud services in the future. For now, authentication is only used for the Feedback feature, otherwise it is not required.", + "spacedrive_cloud_description": "Spacedrive is always local first, but to access our Cloud features, users must register for an account. Note that an account is not required to use any of the base features of Spacedrive, and you can still connect devices to your library without an account.", "spacedrop": "Spacedrop visibility", "spacedrop_a_file": "Spacedrop a File", "spacedrop_already_progress": "Spacedrop already in progress", diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 8e080d56b..620b6ee89 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "src", "declarationDir": "dist" }, - "include": ["src"] + "include": ["src", "../../interface/components/ShowPassword.tsx"] }