Skip to content
This repository has been archived by the owner on Dec 16, 2024. It is now read-only.

Commit

Permalink
feat: implements sign-up pages
Browse files Browse the repository at this point in the history
  • Loading branch information
jspark2000 committed Mar 22, 2024
1 parent 681dc6f commit f5ad470
Show file tree
Hide file tree
Showing 10 changed files with 382 additions and 1 deletion.
6 changes: 6 additions & 0 deletions backend/libs/auth/src/role/roles.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export class RolesGuard implements CanActivate {
return true
}

console.log(request.url)

if (user && request.url === '/api/user') {
return true
}

throw new ForbiddenException('접근 권한이 없습니다')
}
}
124 changes: 124 additions & 0 deletions frontend/src/app/(public)/signup/_components/SignUpForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
'use client'

import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import fetcher from '@/lib/fetcher'
import { SignUpFormSchema } from '@/lib/forms'
import { zodResolver } from '@hookform/resolvers/zod'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import type { z } from 'zod'

export default function SignUpForm() {
const [isFetching, setIsFetching] = useState(false)
const router = useRouter()

const form = useForm<z.infer<typeof SignUpFormSchema>>({
resolver: zodResolver(SignUpFormSchema),
defaultValues: {
username: '',
password: '',
email: '',
nickname: ''
}
})

const onSubmit = async (data: z.infer<typeof SignUpFormSchema>) => {
try {
setIsFetching(true)

await fetcher.post(`/user`, data, false)
router.push(`/signup/verify-email?email=${data.email}`)
} catch (error) {
toast.error('회원가입 실패')
} finally {
setIsFetching(false)
}
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-full space-y-3">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>아이디</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>비밀번호</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>이메일</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel>별명</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-col space-y-2 pt-5">
<Button
variant="accent"
type="submit"
className="w-full"
disabled={isFetching}
>
회원가입
</Button>
<Button
type="button"
className="w-full"
onClick={() => router.push('/')}
>
메인으로
</Button>
</div>
</form>
</Form>
)
}
28 changes: 28 additions & 0 deletions frontend/src/app/(public)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Image from 'next/image'
import SignUpForm from './_components/SignUpForm'

export default function SignUpPage() {
return (
<main className="mx-auto flex w-full max-w-7xl flex-grow flex-col items-center justify-center p-6 lg:px-8">
<div className="flex w-full max-w-[320px] flex-col items-center gap-y-10">
<Image
src="/text-logo.png"
alt="ROYALS"
width={1158}
height={277}
className="hidden h-auto w-full max-w-[320px] dark:inline-block"
priority={true}
/>
<Image
src="/text-logo-light.png"
alt="ROYALS"
width={1158}
height={277}
className="inline-block h-auto w-full max-w-[320px] dark:hidden"
priority={true}
/>
<SignUpForm />
</div>
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client'

import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import fetcher from '@/lib/fetcher'
import { VerifyEmailFormSchema } from '@/lib/forms'
import { zodResolver } from '@hookform/resolvers/zod'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import type { z } from 'zod'

export default function VerifyEmailForm({ email }: { email: string }) {
const [isFetching, setIsFetching] = useState(false)
const router = useRouter()

const form = useForm<z.infer<typeof VerifyEmailFormSchema>>({
resolver: zodResolver(VerifyEmailFormSchema),
defaultValues: {
pin: ''
}
})

const onSubmit = async (data: z.infer<typeof VerifyEmailFormSchema>) => {
try {
setIsFetching(true)
const result = await fetcher.post<{ valid: boolean }>(
`/user/verify-email?email=${email}&pin=${data.pin}`,
{},
false
)

if (!result.valid) throw new Error()

router.push('/login')
toast.success('회원가입이 완료되었습니다')
} catch (error) {
toast.error('인증코드가 일치하지 않습니다')
} finally {
setIsFetching(false)
}
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-full space-y-3">
<FormField
control={form.control}
name="pin"
render={({ field }) => (
<FormItem>
<FormLabel className="sr-only">pin</FormLabel>
<FormControl>
<Input placeholder="인증코드" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
variant="accent"
type="submit"
className="w-full"
disabled={isFetching}
>
이메일 주소 인증
</Button>
</form>
</Form>
)
}
53 changes: 53 additions & 0 deletions frontend/src/app/(public)/signup/verify-email/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Button } from '@/components/ui/button'
import Image from 'next/image'
import Link from 'next/link'
import VerifyEmailForm from './_components/VerifyEmailForm'

export default function VerifyEmailAddressPage({
searchParams
}: {
searchParams?: {
email?: string
}
}) {
return (
<main className="mx-auto flex w-full max-w-7xl flex-grow flex-col items-center justify-center p-6 lg:px-8">
<div className="flex w-full max-w-[320px] flex-col items-center gap-y-10">
{searchParams?.email ? (
<>
<Image
src="/text-logo.png"
alt="ROYALS"
width={1158}
height={277}
className="hidden h-auto w-full max-w-[320px] dark:inline-block"
priority={true}
/>
<Image
src="/text-logo-light.png"
alt="ROYALS"
width={1158}
height={277}
className="inline-block h-auto w-full max-w-[320px] dark:hidden"
priority={true}
/>
<p className="text-md fond-bold text-center">
<span className="text-amber-400">{searchParams.email}</span>으(로)
전송된
<br />
인증코드 6자리를 입력해주세요
</p>
<VerifyEmailForm email={searchParams.email} />
</>
) : (
<>
<h1 className="text-xl font-bold">[ERROR] 잘못된 접근입니다</h1>
<Link href="/">
<Button size="sm">메인으로</Button>
</Link>
</>
)}
</div>
</main>
)
}
62 changes: 62 additions & 0 deletions frontend/src/app/(public)/un-verified/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Button } from '@/components/ui/button'
import { AccountStatus } from '@/lib/enums'
import {
CheckCircleIcon,
ExclamationTriangleIcon
} from '@heroicons/react/24/outline'
import Link from 'next/link'

export default function UnverifiedPage({
searchParams
}: {
searchParams?: {
status?: AccountStatus
}
}) {
const status = searchParams?.status ?? AccountStatus.Disable

const renderStatus = (status: AccountStatus) => {
switch (status) {
case AccountStatus.Verifying:
return (
<div className="flex flex-col space-y-1.5">
<div className="flex flex-row items-center">
<CheckCircleIcon className="mr-1.5 mt-1 h-6 w-6 text-green-500" />
<p>이메일 인증이 완료되었습니다</p>
</div>
<div className="flex flex-row items-center">
<ExclamationTriangleIcon className="mr-1.5 mt-1 h-6 w-6 text-red-500" />
<p>관리자가 아직 회원가입을 승인하지 않았습니다</p>
</div>
</div>
)
default:
return (
<div className="flex flex-col space-y-1.5">
<div className="flex flex-row items-center">
<ExclamationTriangleIcon className="mr-1.5 mt-1 h-6 w-6 text-red-500" />
<p>이메일 인증이 완료되지 않았습니다</p>
</div>
<div className="flex flex-row items-center">
<ExclamationTriangleIcon className="mr-1.5 mt-1 h-6 w-6 text-red-500" />
<p>관리자가 아직 회원가입을 승인하지 않았습니다</p>
</div>
</div>
)
}
}

return (
<main className="mx-auto flex w-full max-w-7xl flex-grow flex-col items-center justify-center p-6 lg:px-8">
<div className="flex w-full flex-col items-center justify-center space-y-5">
<h1 className="text-xl font-bold">
계정 인증절차가 완료되지 않았습니다
</h1>
{renderStatus(status)}
<Link href="/">
<Button variant={'outline'}>메인으로</Button>
</Link>
</div>
</main>
)
}
5 changes: 4 additions & 1 deletion frontend/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const authOptions: NextAuthOptions = {
return {
username: user.username,
role: user.role,
status: user.status,
accessToken,
refreshToken,
accessTokenExpires,
Expand All @@ -83,6 +84,7 @@ export const authOptions: NextAuthOptions = {
if (user) {
token.username = user.username
token.role = user.role
token.status = user.status
token.accessToken = user.accessToken
token.refreshToken = user.refreshToken
token.accessTokenExpires = user.accessTokenExpires
Expand Down Expand Up @@ -117,7 +119,8 @@ export const authOptions: NextAuthOptions = {
session: async ({ session, token }: { session: Session; token: JWT }) => {
session.user = {
username: token.username,
role: token.role
role: token.role,
status: token.status
}
session.token = {
accessToken: token.accessToken,
Expand Down
Loading

0 comments on commit f5ad470

Please sign in to comment.