Singular (admin) account system

This commit is contained in:
Ahmed Al-Taiar
2024-08-14 12:35:57 -04:00
parent 544cea9105
commit b61a80c9a0
37 changed files with 1796 additions and 442 deletions

View File

@@ -0,0 +1,83 @@
import { useEffect, useRef } from 'react'
import { mdiAccount } from '@mdi/js'
import Icon from '@mdi/react'
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
const ForgotPasswordPage = () => {
const { isAuthenticated, forgotPassword } = useAuth()
useEffect(() => {
if (isAuthenticated) navigate(routes.home())
}, [isAuthenticated])
const emailRef = useRef<HTMLInputElement>(null)
useEffect(() => {
emailRef?.current?.focus()
}, [])
const onSubmit = async (data: { username: string }) => {
toast.loading('Processing...', { id: `processing` })
const response = await forgotPassword(data.username)
if (response.error) toast.error(response.error, { id: `processing` })
else {
toast.success(
`A link to reset your password was sent to ${response.email}`,
{ id: `processing` }
)
navigate(routes.login())
}
}
return (
<>
<Metadata title="Forgot Password" />
<div className="mx-auto my-32 flex w-fit flex-wrap items-center justify-center rounded-xl bg-base-100 p-2">
<Form onSubmit={onSubmit} className="space-y-2">
<Label
name="username"
className="input input-bordered flex items-center gap-2"
errorClassName="input input-bordered flex items-center gap-2 input-error"
>
<Label
name="username"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
>
<Icon path={mdiAccount} />
</Label>
<TextField
name="username"
className="grow"
placeholder="Username"
ref={emailRef}
validation={{
required: {
value: true,
message: 'Required',
},
}}
/>
</Label>
<FieldError name="username" className="text-sm text-error" />
<div className="flex w-full">
<Submit className="btn btn-primary mx-auto">Submit</Submit>
</div>
</Form>
</div>
</>
)
}
export default ForgotPasswordPage

View File

@@ -1,13 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react'
import HomePage from './HomePage'
const meta: Meta<typeof HomePage> = {
component: HomePage,
}
export default meta
type Story = StoryObj<typeof HomePage>
export const Primary: Story = {}

View File

@@ -1,14 +0,0 @@
import { render } from '@redwoodjs/testing/web'
import HomePage from './HomePage'
// Improve this test with help from the Redwood Testing Doc:
// https://redwoodjs.com/docs/testing#testing-pages-layouts
describe('HomePage', () => {
it('renders successfully', () => {
expect(() => {
render(<HomePage />)
}).not.toThrow()
})
})

View File

@@ -0,0 +1,115 @@
import { useEffect, useRef } from 'react'
import { mdiAccount, mdiKey } from '@mdi/js'
import Icon from '@mdi/react'
import {
Form,
Label,
TextField,
PasswordField,
Submit,
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
const LoginPage = () => {
const { isAuthenticated, logIn } = useAuth()
useEffect(() => {
if (isAuthenticated) navigate(routes.home())
}, [isAuthenticated])
const emailRef = useRef<HTMLInputElement>(null)
useEffect(() => {
emailRef.current?.focus()
}, [])
const onSubmit = async (data: Record<string, string>) => {
const response = await logIn({
username: data.username,
password: data.password,
})
if (response.message) toast(response.message)
else if (response.error) toast.error(response.error)
else toast.success('Welcome back!')
}
return (
<>
<Metadata title="Login" />
<div className="mx-auto my-32 flex w-fit flex-wrap items-center justify-center rounded-xl bg-base-100 p-2">
<Form onSubmit={onSubmit} className="space-y-2">
<Label
name="username"
className="input input-bordered flex items-center gap-2"
errorClassName="input input-bordered flex items-center gap-2 input-error"
>
<Label
name="username"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
>
<Icon path={mdiAccount} />
</Label>
<TextField
name="username"
className="grow"
placeholder="Username"
validation={{
required: {
value: true,
message: 'Required',
},
}}
/>
</Label>
<FieldError name="username" className="text-sm text-error" />
<Label
name="password"
className="input input-bordered flex items-center gap-2"
errorClassName="input input-bordered flex items-center gap-2 input-error"
>
<Label
name="password"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
>
<Icon path={mdiKey} />
</Label>
<PasswordField
name="password"
className="grow"
placeholder="Password"
autoComplete="current-password"
validation={{
required: {
value: true,
message: 'Required',
},
minLength: {
value: 8,
message: 'Must be 8 or more characters',
},
}}
/>
</Label>
<FieldError name="password" className="text-sm text-error" />
<div className="flex w-full">
<Submit className="btn btn-primary mx-auto">Log In</Submit>
</div>
</Form>
</div>
</>
)
}
export default LoginPage

View File

@@ -1,44 +1,12 @@
import { Link, routes } from '@redwoodjs/router'
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
export default () => (
<main>
<style
dangerouslySetInnerHTML={{
__html: `
html, body {
margin: 0;
}
html * {
box-sizing: border-box;
}
main {
display: flex;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
text-align: center;
background-color: #E2E8F0;
height: 100vh;
}
section {
background-color: white;
border-radius: 0.25rem;
width: 32rem;
padding: 1rem;
margin: 0 auto;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
h1 {
font-size: 2rem;
margin: 0;
font-weight: 500;
line-height: 1;
color: #2D3748;
}
`,
}}
/>
<section>
<h1>
<span>404 Page Not Found</span>
</h1>
</section>
</main>
<div className="flex h-screen items-center justify-center space-x-2 font-inter">
<Link to={routes.home()} className="btn btn-primary">
404
</Link>
<ThemeToggle />
</div>
)

View File

@@ -0,0 +1,119 @@
import { useEffect, useRef, useState } from 'react'
import { mdiKey } from '@mdi/js'
import Icon from '@mdi/react'
import {
Form,
Label,
PasswordField,
Submit,
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } =
useAuth()
const [enabled, setEnabled] = useState(true)
useEffect(() => {
if (isAuthenticated) navigate(routes.home())
}, [isAuthenticated])
useEffect(() => {
const validateToken = async () => {
const response = await validateResetToken(resetToken)
if (response.error) {
setEnabled(false)
toast.error(response.error)
} else {
setEnabled(true)
}
}
validateToken()
}, [resetToken, validateResetToken])
const passwordRef = useRef<HTMLInputElement>(null)
useEffect(() => {
passwordRef.current?.focus()
}, [])
const onSubmit = async (data: Record<string, string>) => {
const response = await resetPassword({
resetToken,
password: data.password,
})
if (response.error) {
toast.error(response.error)
} else {
toast.success('Password changed!')
await reauthenticate()
navigate(routes.login())
}
}
return (
<>
<Metadata title="Reset Password" />
<div className="mx-auto my-32 flex w-fit flex-wrap items-center justify-center rounded-xl bg-base-100 p-2">
<Form onSubmit={onSubmit} className="space-y-2">
<Label
name="password"
className={`${
!enabled ? 'input-disabled' : ''
} input input-bordered flex items-center gap-2`}
errorClassName={`${
!enabled ? 'input-disabled' : ''
} input input-bordered flex items-center gap-2 input-error`}
>
<Label
name="password"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
>
<Icon path={mdiKey} />
</Label>
<PasswordField
name="password"
className="grow"
placeholder="New Password"
ref={passwordRef}
disabled={!enabled}
validation={{
required: {
value: true,
message: 'Required',
},
minLength: {
value: 8,
message: 'Must be 8 or more characters',
},
}}
/>
</Label>
<FieldError name="password" className="text-sm text-error" />
<div className="flex w-full">
<Submit
className={`btn btn-primary mx-auto ${
!enabled ? 'btn-disabled' : ''
}`}
disabled={!enabled}
>
Submit
</Submit>
</div>
</Form>
</div>
</>
)
}
export default ResetPasswordPage

View File

@@ -0,0 +1,163 @@
import { useEffect, useRef } from 'react'
import { mdiEmail, mdiKey, mdiAccount } from '@mdi/js'
import { Icon } from '@mdi/react'
import {
Form,
Label,
TextField,
PasswordField,
Submit,
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
import { Metadata, useQuery } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
const QUERY = gql`
query UserCount {
userCount
}
`
const SignupPage = () => {
const { data, loading } = useQuery(QUERY)
const { isAuthenticated, signUp } = useAuth()
useEffect(() => {
if (!loading && data.userCount >= 1) {
toast.error('Account already exists')
console.log('here - redirect')
console.log(data)
navigate(routes.home())
}
}, [data, loading])
useEffect(() => {
if (isAuthenticated) navigate(routes.home())
}, [isAuthenticated])
const emailRef = useRef<HTMLInputElement>(null)
useEffect(() => {
emailRef.current?.focus()
}, [])
const onSubmit = async (data: Record<string, string>) => {
const response = await signUp({
username: data.username,
password: data.password,
email: data.email.toLowerCase(),
})
if (response.message) toast(response.message)
else if (response.error) toast.error(response.error)
else toast.success('Welcome!')
}
if (loading) return <span className="loading loading-spinner loading-lg" />
return (
<>
<Metadata title="Sign Up" />
<div className="mx-auto my-32 flex w-fit flex-wrap items-center justify-center rounded-xl bg-base-100 p-2">
<Form onSubmit={onSubmit} className="space-y-2">
<Label
name="username"
className="input input-bordered input-disabled flex items-center gap-2"
errorClassName="input input-bordered input-disabled flex items-center gap-2 input-error"
>
<Label
name="username"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
>
<Icon path={mdiAccount} />
</Label>
<TextField
name="username"
className="grow"
value="admin"
readOnly
/>
</Label>
<Label
name="email"
className="input input-bordered flex items-center gap-2"
errorClassName="input input-bordered flex items-center gap-2 input-error"
>
<Label
name="email"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
>
<Icon path={mdiEmail} />
</Label>
<TextField
name="email"
className="grow"
placeholder="Email"
ref={emailRef}
validation={{
required: {
value: true,
message: 'Required',
},
pattern: {
value: new RegExp(
/^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
),
message: 'Invalid email',
},
}}
/>
</Label>
<FieldError name="email" className="text-sm text-error" />
<Label
name="password"
className="input input-bordered flex items-center gap-2"
errorClassName="input input-bordered flex items-center gap-2 input-error"
>
<Label
name="password"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
>
<Icon path={mdiKey} />
</Label>
<PasswordField
name="password"
className="grow"
placeholder="Password"
autoComplete="current-password"
validation={{
required: {
value: true,
message: 'Required',
},
minLength: {
value: 8,
message: 'Must be 8 or more characters',
},
}}
/>
</Label>
<FieldError name="password" className="text-sm text-error" />
<div className="flex w-full">
<Submit className="btn btn-primary mx-auto">Create</Submit>
</div>
</Form>
</div>
</>
)
}
export default SignupPage