diff --git a/studio/components/interfaces/Authentication/Templates.tsx b/studio/components/interfaces/Authentication/Templates.tsx new file mode 100644 index 00000000000..5c193d265e5 --- /dev/null +++ b/studio/components/interfaces/Authentication/Templates.tsx @@ -0,0 +1,207 @@ +import semver from 'semver' +import { FC } from 'react' +import { toJS } from 'mobx' +import { observer } from 'mobx-react-lite' +import { useRouter } from 'next/router' +import { toast } from 'react-hot-toast' +import { Typography } from '@supabase/ui' +import { AutoField, LongTextField } from 'uniforms-bootstrap4' + +import { API_URL } from 'lib/constants' +import { useProjectAuthConfig } from 'hooks' +import { pluckJsonSchemaFields } from 'lib/helpers' +import { patch } from 'lib/common/fetch' +import { authConfig } from 'stores/jsonSchema' +import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel' + +const Templates: FC = ({ project }) => { + const router = useRouter() + const magicLinkEnable = semver.gte( + // @ts-ignore + semver.coerce(toJS(project?.kpsVersion) || 'kps-v0.0.1'), + semver.coerce('kps-v2.5.3') + ) + + const { config, error, mutateAuthConfig } = useProjectAuthConfig(project.ref) + if (error) { + return ( + +

Error connecting to API

+

{`${error}`}

+
+ ) + } + + let model: any = { ...config } + + const onFormSubmit = async (model: any) => { + const response = await patch(`${API_URL}/auth/${router.query.ref}/config`, model) + if (response.error) { + toast.error(`Update config failed: ${response.error.message}`) + } else { + mutateAuthConfig(response) + toast(`Settings saved`) + } + } + + return ( +
+
+ onFormSubmit(model)} + > + + +
+
+ onFormSubmit(model)} + > + + + +
+
+ onFormSubmit(model)} + > + + + +
+ {magicLinkEnable && ( +
+ onFormSubmit(model)} + > + + + +
+ )} +
+ onFormSubmit(model)} + > + + + +
+
+ onFormSubmit(model)} + > + + + +
+
+ ) +} + +export default observer(Templates) diff --git a/studio/components/interfaces/Authentication/Users/InviteUserModal.tsx b/studio/components/interfaces/Authentication/Users/InviteUserModal.tsx new file mode 100644 index 00000000000..e0074346b04 --- /dev/null +++ b/studio/components/interfaces/Authentication/Users/InviteUserModal.tsx @@ -0,0 +1,99 @@ +import { useContext, useState, useRef } from 'react' +import { observer } from 'mobx-react-lite' +import { toast } from 'react-hot-toast' +import { Button, Modal, Input, IconPlus, IconMail } from '@supabase/ui' + +import { post } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { PageContext } from 'pages/project/[ref]/auth/users' + +const InviteUserModal = () => { + const PageState: any = useContext(PageContext) + const inputRef = useRef(null) + const [visible, setVisible] = useState(false) + const [loading, setLoading] = useState(false) + const [emailValue, setEmailValue] = useState('') + + function handleToggle() { + // reset data before showing modal again + if (!visible) { + setEmailValue('') + } + setVisible(!visible) + } + + function onInputChange(e: any) { + setEmailValue(e.target.value) + } + + async function onInviteUser() { + const emailValidateRegex = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/ + if (!emailValidateRegex.test(emailValue)) { + // focus input + ;(inputRef?.current as any)?.focus() + // show error + let message = `${emailValue} is an invalid email` + if (emailValue.trim() == '') message = 'Please enter a valid email' + toast.error(message) + return + } + + try { + setLoading(true) + const response = await post(`${API_URL}/auth/${PageState.projectRef}/invite`, { + email: emailValue, + }) + if (response.error) { + toast.error(`Inviting user failed: ${response.error.message}`) + } else { + PageState.fetchData(1) + toast(`Sent invite email to ${emailValue}`) + } + } catch (error) { + console.error('onInviteUser error:', error) + toast.error(`Inviting user failed`) + } finally { + setLoading(false) + setVisible(false) + } + } + + return ( +
+ + + } + autoFocus + // @ts-ignore + ref={inputRef} + value={emailValue} + onChange={onInputChange} + type="text" + name="email" + id="email" + placeholder="User email" + className="w-full" + /> + + + +
+ ) +} + +export default observer(InviteUserModal) diff --git a/studio/components/interfaces/Authentication/Users/UserDropdown.tsx b/studio/components/interfaces/Authentication/Users/UserDropdown.tsx new file mode 100644 index 00000000000..3a8836e8274 --- /dev/null +++ b/studio/components/interfaces/Authentication/Users/UserDropdown.tsx @@ -0,0 +1,124 @@ +import { FC, useContext, useState } from 'react' +import { toast } from 'react-hot-toast' +import { observer } from 'mobx-react-lite' +import { Button, Dropdown, Divider, IconTrash, IconMail, IconMoreHorizontal } from '@supabase/ui' + +import { timeout } from 'lib/helpers' +import { post, delete_ } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { PageContext } from 'pages/project/[ref]/auth/users' +import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' + +const UserDropdown: FC<{ user: any }> = ({ user }) => { + const PageState: any = useContext(PageContext) + const [loading, setLoading] = useState(false) + + async function handleResetPassword() { + try { + setLoading(true) + const response = await post(`${API_URL}/auth/${PageState.projectRef}/recover`, user) + if (response.error) { + toast.error(`Send password recovery failed: ${response.error.message}`) + } else { + toast(`Sent password recovery to ${user.email}`) + } + } catch (error) { + console.error('handleResetPassword error:', error) + toast.error(`Send password recovery failed`) + } finally { + setLoading(false) + } + } + + async function handleSendMagicLink() { + try { + setLoading(true) + const response = await post(`${API_URL}/auth/${PageState.projectRef}/magiclink`, user) + if (response.error) { + toast.error(`Sending magic link failed: ${response.error.message}`) + } else { + toast(`Sent magic link to ${user.email}`) + } + } catch (error) { + console.error('handleSendMagicLink error:', error) + toast.error(`Sending magic link failed`) + } finally { + setLoading(false) + } + } + + async function handleSendOtp() { + try { + setLoading(true) + const response = await post(`${API_URL}/auth/${PageState.projectRef}/otp`, user) + if (response.error) { + toast.error(`Sending OTP failed: ${response.error.message}`) + } else { + toast(`Sent OTP to ${user.phone}`) + } + } catch (error) { + console.error('handleSendOtp error:', error) + toast.error(`Sending OTP failed`) + } finally { + setLoading(false) + } + } + + async function handleDelete() { + await timeout(200) + + confirmAlert({ + title: 'Confirm to delete', + message: `This is permanent! Are you sure you want to delete user ${user.email} ?`, + onAsyncConfirm: async () => { + setLoading(true) + const response = await delete_(`${API_URL}/auth/${PageState.projectRef}/users`, user) + if (response.error) { + toast.error(`Deleting user failed: ${response.error.message}`) + } else { + toast(`User ${user.email} deleted`) + PageState.users = PageState.users.filter((x: any) => x.id != user.id) + } + setLoading(false) + }, + }) + } + + return ( + + {user.email !== null ? ( + <> + }> + Send password recovery + + }> + Send magic link + + + ) : null} + {user.phone !== null ? ( + }> + Send OTP + + ) : null} + + }> + Delete user + + + } + > +