mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Start refactoring the auth pages to break them down into separate smaller components
This commit is contained in:
1 parent
0ecdea2a43
commit
4caaa0a1c4
11 files changed
+678
-625
No files matched your search
@@ -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<any> = ({ 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 (
|
||||
<Typography.Text type="danger">
|
||||
<p>Error connecting to API</p>
|
||||
<p>{`${error}`}</p>
|
||||
</Typography.Text>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="">
|
||||
<div className="my-8 mt-0">
|
||||
<SchemaFormPanel
|
||||
title="SMS Message"
|
||||
schema={pluckJsonSchemaFields(authConfig, ['SMS_TEMPLATE'])}
|
||||
model={{
|
||||
SMS_TEMPLATE: model.SMS_TEMPLATE,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="SMS_TEMPLATE"
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an sms body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<SchemaFormPanel
|
||||
title="Confirm Signup"
|
||||
schema={pluckJsonSchemaFields(authConfig, [
|
||||
'MAILER_SUBJECTS_CONFIRMATION',
|
||||
'MAILER_TEMPLATES_CONFIRMATION_CONTENT',
|
||||
])}
|
||||
model={{
|
||||
MAILER_SUBJECTS_CONFIRMATION: model.MAILER_SUBJECTS_CONFIRMATION,
|
||||
MAILER_TEMPLATES_CONFIRMATION_CONTENT: model.MAILER_TEMPLATES_CONFIRMATION_CONTENT,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="MAILER_SUBJECTS_CONFIRMATION"
|
||||
showInlineError
|
||||
errorMessage="Please enter a subject."
|
||||
/>
|
||||
<LongTextField
|
||||
name="MAILER_TEMPLATES_CONFIRMATION_CONTENT"
|
||||
rows={6}
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<SchemaFormPanel
|
||||
title="Reset password"
|
||||
schema={pluckJsonSchemaFields(authConfig, [
|
||||
'MAILER_SUBJECTS_RECOVERY',
|
||||
'MAILER_TEMPLATES_RECOVERY_CONTENT',
|
||||
])}
|
||||
model={{
|
||||
MAILER_SUBJECTS_RECOVERY: model.MAILER_SUBJECTS_RECOVERY,
|
||||
MAILER_TEMPLATES_RECOVERY_CONTENT: model.MAILER_TEMPLATES_RECOVERY_CONTENT,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="MAILER_SUBJECTS_RECOVERY"
|
||||
showInlineError
|
||||
errorMessage="Please enter a subject."
|
||||
/>
|
||||
<LongTextField
|
||||
name="MAILER_TEMPLATES_RECOVERY_CONTENT"
|
||||
rows={6}
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
{magicLinkEnable && (
|
||||
<div className="my-8">
|
||||
<SchemaFormPanel
|
||||
title="Magic Link"
|
||||
schema={pluckJsonSchemaFields(authConfig, [
|
||||
'MAILER_SUBJECTS_MAGIC_LINK',
|
||||
'MAILER_TEMPLATES_MAGIC_LINK_CONTENT',
|
||||
])}
|
||||
model={{
|
||||
MAILER_SUBJECTS_MAGIC_LINK: model.MAILER_SUBJECTS_MAGIC_LINK,
|
||||
MAILER_TEMPLATES_MAGIC_LINK_CONTENT: model.MAILER_TEMPLATES_MAGIC_LINK_CONTENT,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="MAILER_SUBJECTS_MAGIC_LINK"
|
||||
showInlineError
|
||||
errorMessage="Please enter a subject."
|
||||
/>
|
||||
<LongTextField
|
||||
name="MAILER_TEMPLATES_MAGIC_LINK_CONTENT"
|
||||
rows={6}
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
)}
|
||||
<div className="my-8">
|
||||
<SchemaFormPanel
|
||||
title="Change email address"
|
||||
schema={pluckJsonSchemaFields(authConfig, [
|
||||
'MAILER_SUBJECTS_EMAIL_CHANGE',
|
||||
'MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT',
|
||||
])}
|
||||
model={{
|
||||
MAILER_SUBJECTS_EMAIL_CHANGE: model.MAILER_SUBJECTS_EMAIL_CHANGE,
|
||||
MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT: model.MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="MAILER_SUBJECTS_EMAIL_CHANGE"
|
||||
showInlineError
|
||||
errorMessage="Please enter a subject."
|
||||
/>
|
||||
<LongTextField
|
||||
name="MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT"
|
||||
rows={6}
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<SchemaFormPanel
|
||||
title="Invite user"
|
||||
schema={pluckJsonSchemaFields(authConfig, [
|
||||
'MAILER_SUBJECTS_INVITE',
|
||||
'MAILER_TEMPLATES_INVITE_CONTENT',
|
||||
])}
|
||||
model={{
|
||||
MAILER_SUBJECTS_INVITE: model.MAILER_SUBJECTS_INVITE,
|
||||
MAILER_TEMPLATES_INVITE_CONTENT: model.MAILER_TEMPLATES_INVITE_CONTENT,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="MAILER_SUBJECTS_INVITE"
|
||||
showInlineError
|
||||
errorMessage="Please enter a subject."
|
||||
/>
|
||||
<LongTextField
|
||||
name="MAILER_TEMPLATES_INVITE_CONTENT"
|
||||
rows={6}
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Templates)
|
||||
@@ -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 (
|
||||
<div>
|
||||
<Button onClick={handleToggle} icon={<IconPlus />}>
|
||||
Invite
|
||||
</Button>
|
||||
<Modal
|
||||
size="small"
|
||||
key="invite-user-modal"
|
||||
visible={visible}
|
||||
title="Invite new user"
|
||||
hideFooter
|
||||
onCancel={handleToggle}
|
||||
closable
|
||||
>
|
||||
<Input
|
||||
label="User email"
|
||||
icon={<IconMail />}
|
||||
autoFocus
|
||||
// @ts-ignore
|
||||
ref={inputRef}
|
||||
value={emailValue}
|
||||
onChange={onInputChange}
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="User email"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<Button onClick={onInviteUser} loading={loading} disabled={loading} block>
|
||||
Invite user
|
||||
</Button>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(InviteUserModal)
|
||||
@@ -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 (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<>
|
||||
{user.email !== null ? (
|
||||
<>
|
||||
<Dropdown.Item onClick={handleResetPassword} icon={<IconMail size="tiny" />}>
|
||||
Send password recovery
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={handleSendMagicLink} icon={<IconMail size="tiny" />}>
|
||||
Send magic link
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
) : null}
|
||||
{user.phone !== null ? (
|
||||
<Dropdown.Item onClick={handleSendOtp} icon={<IconMail size="tiny" />}>
|
||||
Send OTP
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
<Divider light />
|
||||
<Dropdown.Item onClick={handleDelete} icon={<IconTrash size="tiny" />}>
|
||||
Delete user
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
as="span"
|
||||
type="text"
|
||||
icon={<IconMoreHorizontal />}
|
||||
loading={loading}
|
||||
className="hover:border-gray-500"
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(UserDropdown)
|
||||
@@ -0,0 +1,80 @@
|
||||
import semver from 'semver'
|
||||
import { useEffect, useContext } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Input, IconSearch, IconX } from '@supabase/ui'
|
||||
|
||||
import { PageContext } from 'pages/project/[ref]/auth/users'
|
||||
import UsersList from './UsersList'
|
||||
import InviteUserModal from './InviteUserModal'
|
||||
|
||||
const Users = () => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
const inviteEnabled = semver.gte(
|
||||
// @ts-ignore
|
||||
semver.coerce(PageState.projectKpsVersion || 'kps-v2.5.4'),
|
||||
semver.coerce('kps-v2.5.3')
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
PageState.fetchData(1)
|
||||
}, [])
|
||||
|
||||
function onFilterChange(e: any) {
|
||||
PageState.filterInputValue = e.target.value
|
||||
}
|
||||
|
||||
function onFilterKeyPress(e: any) {
|
||||
// enter key
|
||||
if (e.keyCode == 13) onSearchUser()
|
||||
}
|
||||
|
||||
function onSearchUser() {
|
||||
PageState.filterKeywords = PageState.filterInputValue
|
||||
PageState.fetchData(1)
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
PageState.filterInputValue = ''
|
||||
PageState.filterKeywords = ''
|
||||
PageState.fetchData(1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex justify-between">
|
||||
<div className="relative flex space-x-1">
|
||||
<Input
|
||||
size="tiny"
|
||||
value={PageState.filterInputValue}
|
||||
onChange={onFilterChange}
|
||||
onKeyDown={onFilterKeyPress}
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="Search by email"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
actions={[
|
||||
PageState.filterInputValue && (
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
icon={<IconX size="tiny" />}
|
||||
onClick={() => clearSearch()}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{inviteEnabled && <InviteUserModal />}
|
||||
</div>
|
||||
<section className="overflow-visible mt-4">
|
||||
<div className="relative section-block--body rounded">
|
||||
<div className="align-middle inline-block min-w-full">
|
||||
<UsersList />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Users)
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useContext } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Loading } from '@supabase/ui'
|
||||
|
||||
import { PageContext } from 'pages/project/[ref]/auth/users'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import UserListItem from './UsersListItem'
|
||||
import UsersPagination from './UsersPagination'
|
||||
|
||||
const UsersList = ({}) => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
|
||||
return (
|
||||
<Loading active={PageState.usersLoading}>
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th>Email</Table.th>
|
||||
<Table.th>Phone</Table.th>
|
||||
<Table.th className="hidden 2xl:table-cell">Provider</Table.th>
|
||||
<Table.th className="hidden 2xl:table-cell">Created</Table.th>
|
||||
<Table.th className="hidden xl:table-cell">Last Sign In</Table.th>
|
||||
<Table.th className="hidden lg:table-cell">User UID</Table.th>
|
||||
<Table.th></Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
{PageState.users.length == 0 && (
|
||||
<Table.tr>
|
||||
{/* @ts-ignore */}
|
||||
<Table.td
|
||||
colSpan={7}
|
||||
className="h-28 p-4 whitespace-nowrap border-t leading-5 text-gray-300 text-sm"
|
||||
></Table.td>
|
||||
</Table.tr>
|
||||
)}
|
||||
{PageState.users.length > 0 &&
|
||||
PageState.users.map((x: any) => <UserListItem key={x.id} user={x} />)}
|
||||
<Table.tr>
|
||||
<Table.td colSpan={7}>
|
||||
<UsersPagination />
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(UsersList)
|
||||
@@ -0,0 +1,55 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { FC } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Badge, Typography } from '@supabase/ui'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
|
||||
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
||||
import UserDropdown from './UserDropdown'
|
||||
|
||||
interface Props {
|
||||
user: any
|
||||
}
|
||||
|
||||
const UserListItem: FC<Props> = ({ user }) => {
|
||||
const isUserConfirmed = user.email_confirmed_at || user.phone_confirmed_at
|
||||
return (
|
||||
<Table.tr key={user.id}>
|
||||
<Table.td className="whitespace-nowrap">
|
||||
<Typography.Text>{!user.email ? '-' : user.email}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className="whitespace-nowrap">
|
||||
<Typography.Text>{!user.phone ? '-' : user.phone}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className="hidden 2xl:table-cell">
|
||||
<Typography.Text type="secondary" className="capitalize">
|
||||
{user?.raw_app_meta_data?.provider || user?.app_metadata?.provider}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className="hidden 2xl:table-cell">
|
||||
<Typography.Text type="secondary" small>
|
||||
{dayjs(user.created_at).format('DD MMM, YYYY HH:mm')}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className="hidden xl:table-cell">
|
||||
{!isUserConfirmed ? (
|
||||
<Badge color="yellow">Waiting for verification..</Badge>
|
||||
) : user.last_sign_in_at ? (
|
||||
dayjs(user.last_sign_in_at).format('DD MMM, YYYY HH:mm')
|
||||
) : (
|
||||
'Never'
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td className="hidden lg:table-cell">
|
||||
<SimpleCodeBlock metastring="" className="font-xs bash">
|
||||
{user.id}
|
||||
</SimpleCodeBlock>
|
||||
</Table.td>
|
||||
<Table.td className="text-right">
|
||||
<UserDropdown user={user} />
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(UserListItem)
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useContext } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button } from '@supabase/ui'
|
||||
|
||||
import { PageContext } from 'pages/project/[ref]/auth/users'
|
||||
|
||||
const UsersPagination = () => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
|
||||
function onNext() {
|
||||
PageState.fetchData(PageState.page + 1)
|
||||
}
|
||||
|
||||
function onPrevious() {
|
||||
PageState.fetchData(PageState.page - 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="flex items-center justify-between overflow-hidden" aria-label="Pagination">
|
||||
<div className="hidden sm:block">
|
||||
<p className="text-xs text-gray-400">
|
||||
Showing
|
||||
<span className="px-1 font-medium text-gray-400">{PageState.fromRow}</span>
|
||||
to
|
||||
<span className="px-1 font-medium text-gray-400">{PageState.toRow}</span>
|
||||
of
|
||||
<span className="px-1 font-medium text-gray-400">{PageState.totalUsers}</span>
|
||||
results
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 flex justify-between sm:justify-end">
|
||||
{PageState.hasPrevious && (
|
||||
<Button type="secondary" disabled={!PageState.hasPrevious} onClick={onPrevious}>
|
||||
Previous
|
||||
</Button>
|
||||
)}
|
||||
{PageState.hasNext && (
|
||||
<Button type="secondary" disabled={!PageState.hasNext} className="ml-3" onClick={onNext}>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(UsersPagination)
|
||||
@@ -0,0 +1,2 @@
|
||||
import Users from './Users'
|
||||
export default Users
|
||||
@@ -0,0 +1,4 @@
|
||||
import Users from './Users'
|
||||
import Templates from './Templates'
|
||||
|
||||
export { Users, Templates }
|
||||
@@ -1,19 +1,7 @@
|
||||
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 { withAuth, useStore, useProjectAuthConfig } from 'hooks'
|
||||
import { pluckJsonSchemaFields } from 'lib/helpers'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { authConfig } from 'stores/jsonSchema'
|
||||
import { withAuth, useStore } from 'hooks'
|
||||
import { AuthLayout } from 'components/layouts'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
import { Templates } from 'components/interfaces/Authentication'
|
||||
|
||||
const Auth = () => {
|
||||
const { ui } = useStore()
|
||||
@@ -27,194 +15,5 @@ const Auth = () => {
|
||||
</AuthLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(observer(Auth))
|
||||
|
||||
const Templates: FC<any> = observer(({ 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 (
|
||||
<Typography.Text type="danger">
|
||||
<p>Error connecting to API</p>
|
||||
<p>{`${error}`}</p>
|
||||
</Typography.Text>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="">
|
||||
<div className="my-8 mt-0">
|
||||
<SchemaFormPanel
|
||||
title="SMS Message"
|
||||
schema={pluckJsonSchemaFields(authConfig, ['SMS_TEMPLATE'])}
|
||||
model={{
|
||||
SMS_TEMPLATE: model.SMS_TEMPLATE,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="SMS_TEMPLATE"
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an sms body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<SchemaFormPanel
|
||||
title="Confirm Signup"
|
||||
schema={pluckJsonSchemaFields(authConfig, [
|
||||
'MAILER_SUBJECTS_CONFIRMATION',
|
||||
'MAILER_TEMPLATES_CONFIRMATION_CONTENT',
|
||||
])}
|
||||
model={{
|
||||
MAILER_SUBJECTS_CONFIRMATION: model.MAILER_SUBJECTS_CONFIRMATION,
|
||||
MAILER_TEMPLATES_CONFIRMATION_CONTENT: model.MAILER_TEMPLATES_CONFIRMATION_CONTENT,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="MAILER_SUBJECTS_CONFIRMATION"
|
||||
showInlineError
|
||||
errorMessage="Please enter a subject."
|
||||
/>
|
||||
<LongTextField
|
||||
name="MAILER_TEMPLATES_CONFIRMATION_CONTENT"
|
||||
rows={6}
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<SchemaFormPanel
|
||||
title="Reset password"
|
||||
schema={pluckJsonSchemaFields(authConfig, [
|
||||
'MAILER_SUBJECTS_RECOVERY',
|
||||
'MAILER_TEMPLATES_RECOVERY_CONTENT',
|
||||
])}
|
||||
model={{
|
||||
MAILER_SUBJECTS_RECOVERY: model.MAILER_SUBJECTS_RECOVERY,
|
||||
MAILER_TEMPLATES_RECOVERY_CONTENT: model.MAILER_TEMPLATES_RECOVERY_CONTENT,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="MAILER_SUBJECTS_RECOVERY"
|
||||
showInlineError
|
||||
errorMessage="Please enter a subject."
|
||||
/>
|
||||
<LongTextField
|
||||
name="MAILER_TEMPLATES_RECOVERY_CONTENT"
|
||||
rows={6}
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
{magicLinkEnable && (
|
||||
<div className="my-8">
|
||||
<SchemaFormPanel
|
||||
title="Magic Link"
|
||||
schema={pluckJsonSchemaFields(authConfig, [
|
||||
'MAILER_SUBJECTS_MAGIC_LINK',
|
||||
'MAILER_TEMPLATES_MAGIC_LINK_CONTENT',
|
||||
])}
|
||||
model={{
|
||||
MAILER_SUBJECTS_MAGIC_LINK: model.MAILER_SUBJECTS_MAGIC_LINK,
|
||||
MAILER_TEMPLATES_MAGIC_LINK_CONTENT: model.MAILER_TEMPLATES_MAGIC_LINK_CONTENT,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="MAILER_SUBJECTS_MAGIC_LINK"
|
||||
showInlineError
|
||||
errorMessage="Please enter a subject."
|
||||
/>
|
||||
<LongTextField
|
||||
name="MAILER_TEMPLATES_MAGIC_LINK_CONTENT"
|
||||
rows={6}
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
)}
|
||||
<div className="my-8">
|
||||
<SchemaFormPanel
|
||||
title="Change email address"
|
||||
schema={pluckJsonSchemaFields(authConfig, [
|
||||
'MAILER_SUBJECTS_EMAIL_CHANGE',
|
||||
'MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT',
|
||||
])}
|
||||
model={{
|
||||
MAILER_SUBJECTS_EMAIL_CHANGE: model.MAILER_SUBJECTS_EMAIL_CHANGE,
|
||||
MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT: model.MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="MAILER_SUBJECTS_EMAIL_CHANGE"
|
||||
showInlineError
|
||||
errorMessage="Please enter a subject."
|
||||
/>
|
||||
<LongTextField
|
||||
name="MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT"
|
||||
rows={6}
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<SchemaFormPanel
|
||||
title="Invite user"
|
||||
schema={pluckJsonSchemaFields(authConfig, [
|
||||
'MAILER_SUBJECTS_INVITE',
|
||||
'MAILER_TEMPLATES_INVITE_CONTENT',
|
||||
])}
|
||||
model={{
|
||||
MAILER_SUBJECTS_INVITE: model.MAILER_SUBJECTS_INVITE,
|
||||
MAILER_TEMPLATES_INVITE_CONTENT: model.MAILER_TEMPLATES_INVITE_CONTENT,
|
||||
}}
|
||||
onSubmit={(model: any) => onFormSubmit(model)}
|
||||
>
|
||||
<AutoField
|
||||
name="MAILER_SUBJECTS_INVITE"
|
||||
showInlineError
|
||||
errorMessage="Please enter a subject."
|
||||
/>
|
||||
<LongTextField
|
||||
name="MAILER_TEMPLATES_INVITE_CONTENT"
|
||||
rows={6}
|
||||
showInlineError
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,37 +1,15 @@
|
||||
import React, { createContext, useRef, useContext, useEffect, useState, FC } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import semver from 'semver'
|
||||
import { toJS } from 'mobx'
|
||||
import React, { createContext, useContext, useEffect } from 'react'
|
||||
import { observer, useLocalObservable } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import toast from 'react-hot-toast'
|
||||
import { get, delete_, post } from 'lib/common/fetch'
|
||||
import { timeout } from 'lib/helpers'
|
||||
import {
|
||||
Button,
|
||||
IconMoreHorizontal,
|
||||
IconPlus,
|
||||
Input,
|
||||
IconSearch,
|
||||
Badge,
|
||||
Typography,
|
||||
Loading,
|
||||
IconX,
|
||||
Dropdown,
|
||||
Divider,
|
||||
IconTrash,
|
||||
Modal,
|
||||
IconMail,
|
||||
} from '@supabase/ui'
|
||||
import { get } from 'lib/common/fetch'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { withAuth, useStore } from 'hooks'
|
||||
import { AuthLayout } from 'components/layouts'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import { Users } from 'components/interfaces/Authentication'
|
||||
|
||||
export const PageContext = createContext(null)
|
||||
|
||||
const PageContext = createContext(null)
|
||||
const Page = () => {
|
||||
const PageState: any = useLocalObservable(() => ({
|
||||
projectRef: undefined,
|
||||
@@ -118,397 +96,3 @@ const PageLayout = observer(() => {
|
||||
</AuthLayout>
|
||||
)
|
||||
})
|
||||
|
||||
const Users = observer(() => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
const inviteEnabled = semver.gte(
|
||||
// @ts-ignore
|
||||
semver.coerce(toJS(PageState.projectKpsVersion) || 'kps-v2.5.4'),
|
||||
semver.coerce('kps-v2.5.3')
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
PageState.fetchData(1)
|
||||
}, [])
|
||||
|
||||
function onFilterChange(e: any) {
|
||||
PageState.filterInputValue = e.target.value
|
||||
}
|
||||
|
||||
function onFilterKeyPress(e: any) {
|
||||
// enter key
|
||||
if (e.keyCode == 13) onSearchUser()
|
||||
}
|
||||
|
||||
function onSearchUser() {
|
||||
PageState.filterKeywords = PageState.filterInputValue
|
||||
PageState.fetchData(1)
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
PageState.filterInputValue = ''
|
||||
PageState.filterKeywords = ''
|
||||
PageState.fetchData(1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex justify-between">
|
||||
<div className="relative flex space-x-1">
|
||||
<Input
|
||||
size="tiny"
|
||||
value={PageState.filterInputValue}
|
||||
onChange={onFilterChange}
|
||||
onKeyDown={onFilterKeyPress}
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="Search by email"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
actions={[
|
||||
PageState.filterInputValue && (
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
icon={<IconX size="tiny" />}
|
||||
onClick={() => clearSearch()}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{inviteEnabled && <InviteUserModal />}
|
||||
</div>
|
||||
<section className="overflow-visible mt-4">
|
||||
<div className="relative section-block--body rounded">
|
||||
<div className="align-middle inline-block min-w-full">
|
||||
<UserList />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const UsersPagination = observer(() => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
|
||||
function onNext() {
|
||||
PageState.fetchData(PageState.page + 1)
|
||||
}
|
||||
|
||||
function onPrevious() {
|
||||
PageState.fetchData(PageState.page - 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="flex items-center justify-between overflow-hidden" aria-label="Pagination">
|
||||
<div className="hidden sm:block">
|
||||
<p className="text-xs text-gray-400">
|
||||
Showing
|
||||
<span className="px-1 font-medium text-gray-400">{PageState.fromRow}</span>
|
||||
to
|
||||
<span className="px-1 font-medium text-gray-400">{PageState.toRow}</span>
|
||||
of
|
||||
<span className="px-1 font-medium text-gray-400">{PageState.totalUsers}</span>
|
||||
results
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 flex justify-between sm:justify-end">
|
||||
{PageState.hasPrevious && (
|
||||
<Button type="secondary" disabled={!PageState.hasPrevious} onClick={onPrevious}>
|
||||
Previous
|
||||
</Button>
|
||||
)}
|
||||
{PageState.hasNext && (
|
||||
<Button type="secondary" disabled={!PageState.hasNext} className="ml-3" onClick={onNext}>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
})
|
||||
|
||||
const InviteUserModal = observer(() => {
|
||||
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 (
|
||||
<div>
|
||||
<Button onClick={handleToggle} icon={<IconPlus />}>
|
||||
Invite
|
||||
</Button>
|
||||
<Modal
|
||||
size="small"
|
||||
key="invite-user-modal"
|
||||
visible={visible}
|
||||
title="Invite new user"
|
||||
hideFooter
|
||||
onCancel={handleToggle}
|
||||
closable
|
||||
>
|
||||
<Input
|
||||
label="User email"
|
||||
icon={<IconMail />}
|
||||
autoFocus
|
||||
// @ts-ignore
|
||||
ref={inputRef}
|
||||
value={emailValue}
|
||||
onChange={onInputChange}
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="User email"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<Button onClick={onInviteUser} loading={loading} disabled={loading} block>
|
||||
Invite user
|
||||
</Button>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const UserList = observer(({}) => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
|
||||
return (
|
||||
<Loading active={PageState.usersLoading}>
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th>Email</Table.th>
|
||||
<Table.th>Phone</Table.th>
|
||||
<Table.th className="hidden 2xl:table-cell">Provider</Table.th>
|
||||
<Table.th className="hidden 2xl:table-cell">Created</Table.th>
|
||||
<Table.th className="hidden xl:table-cell">Last Sign In</Table.th>
|
||||
<Table.th className="hidden lg:table-cell">User UID</Table.th>
|
||||
<Table.th></Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
{PageState.users.length == 0 && (
|
||||
<Table.tr>
|
||||
{/* @ts-ignore */}
|
||||
<Table.td
|
||||
colSpan={7}
|
||||
className="h-28 p-4 whitespace-nowrap border-t leading-5 text-gray-300 text-sm"
|
||||
></Table.td>
|
||||
</Table.tr>
|
||||
)}
|
||||
{PageState.users.length > 0 &&
|
||||
PageState.users.map((x: any) => <UserListItem key={x.id} user={x} />)}
|
||||
<Table.tr>
|
||||
<Table.td colSpan={7}>
|
||||
<UsersPagination />
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Loading>
|
||||
)
|
||||
})
|
||||
|
||||
const UserListItem: FC<{ user: any }> = observer(({ user }) => {
|
||||
const isUserConfirmed = user.email_confirmed_at || user.phone_confirmed_at
|
||||
return (
|
||||
<Table.tr key={user.id}>
|
||||
<Table.td className="whitespace-nowrap">
|
||||
<Typography.Text>{!user.email ? '-' : user.email}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className="whitespace-nowrap">
|
||||
<Typography.Text>{!user.phone ? '-' : user.phone}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className="hidden 2xl:table-cell">
|
||||
<Typography.Text type="secondary" className="capitalize">
|
||||
{user?.raw_app_meta_data?.provider || user?.app_metadata?.provider}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className="hidden 2xl:table-cell">
|
||||
<Typography.Text type="secondary" small>
|
||||
{dayjs(user.created_at).format('DD MMM, YYYY HH:mm')}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className="hidden xl:table-cell">
|
||||
{!isUserConfirmed ? (
|
||||
<Badge color="yellow">Waiting for verification..</Badge>
|
||||
) : user.last_sign_in_at ? (
|
||||
dayjs(user.last_sign_in_at).format('DD MMM, YYYY HH:mm')
|
||||
) : (
|
||||
'Never'
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td className="hidden lg:table-cell">
|
||||
<SimpleCodeBlock metastring="" className="font-xs bash">
|
||||
{user.id}
|
||||
</SimpleCodeBlock>
|
||||
</Table.td>
|
||||
<Table.td className="text-right">
|
||||
<UserDropdown user={user} />
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
})
|
||||
|
||||
const UserDropdown: FC<{ user: any }> = observer(({ 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 (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<>
|
||||
{user.email !== null ? (
|
||||
<>
|
||||
<Dropdown.Item onClick={handleResetPassword} icon={<IconMail size="tiny" />}>
|
||||
Send password recovery
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={handleSendMagicLink} icon={<IconMail size="tiny" />}>
|
||||
Send magic link
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
) : null}
|
||||
{user.phone !== null ? (
|
||||
<Dropdown.Item onClick={handleSendOtp} icon={<IconMail size="tiny" />}>
|
||||
Send OTP
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
<Divider light />
|
||||
<Dropdown.Item onClick={handleDelete} icon={<IconTrash size="tiny" />}>
|
||||
Delete user
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
as="span"
|
||||
type="text"
|
||||
icon={<IconMoreHorizontal />}
|
||||
loading={loading}
|
||||
className="hover:border-gray-500"
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user