diff --git a/README.md b/README.md index ce84784..79a6701 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ This template ships with OpenAI `gpt-3.5-turbo` as the default. However, thanks You can deploy your own version of the Next.js AI Chatbot to Vercel with one click: -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_SECRET&envDescription=How+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&teamCreateStatus=hidden&stores=[{"type":"kv"},{"type":"postgres"}]) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_SECRET&envDescription=How+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&teamCreateStatus=hidden&stores=[{"type":"kv"}]) ## Creating a KV Database Instance @@ -57,7 +57,6 @@ You will need to use the environment variables [defined in `.env.example`](.env. ```bash pnpm install -pnpm seed pnpm dev ``` diff --git a/app/login/actions.ts b/app/login/actions.ts index 7bb83bf..f23e220 100644 --- a/app/login/actions.ts +++ b/app/login/actions.ts @@ -1,14 +1,26 @@ 'use server' import { signIn } from '@/auth' -import { AuthResult } from '@/lib/types' +import { User } from '@/lib/types' import { AuthError } from 'next-auth' import { z } from 'zod' +import { kv } from '@vercel/kv' +import { ResultCode } from '@/lib/utils' + +export async function getUser(email: string) { + const user = await kv.hgetall(`user:${email}`) + return user +} + +interface Result { + type: string + resultCode: ResultCode +} export async function authenticate( - _prevState: AuthResult | undefined, + _prevState: Result | undefined, formData: FormData -) { +): Promise { try { const email = formData.get('email') const password = formData.get('password') @@ -27,24 +39,33 @@ export async function authenticate( await signIn('credentials', { email, password, - redirectTo: '/' + redirect: false }) + + return { + type: 'success', + resultCode: ResultCode.UserLoggedIn + } } else { - return { type: 'error', message: 'Invalid credentials!' } + return { + type: 'error', + resultCode: ResultCode.InvalidCredentials + } } } catch (error) { if (error instanceof AuthError) { switch (error.type) { case 'CredentialsSignin': - return { type: 'error', message: 'Invalid credentials!' } + return { + type: 'error', + resultCode: ResultCode.InvalidCredentials + } default: return { type: 'error', - message: 'Something went wrong, please try again!' + resultCode: ResultCode.UnknownError } } } - - throw error } } diff --git a/app/signup/actions.ts b/app/signup/actions.ts index 7bddb74..492586a 100644 --- a/app/signup/actions.ts +++ b/app/signup/actions.ts @@ -1,15 +1,50 @@ 'use server' import { signIn } from '@/auth' -import { db } from '@vercel/postgres' -import { getStringFromBuffer } from '@/lib/utils' +import { ResultCode, getStringFromBuffer } from '@/lib/utils' import { z } from 'zod' -import { AuthResult } from '@/lib/types' +import { kv } from '@vercel/kv' +import { getUser } from '../login/actions' +import { AuthError } from 'next-auth' + +export async function createUser( + email: string, + hashedPassword: string, + salt: string +) { + const existingUser = await getUser(email) + + if (existingUser) { + return { + type: 'error', + resultCode: ResultCode.UserAlreadyExists + } + } else { + const user = { + id: crypto.randomUUID(), + email, + password: hashedPassword, + salt + } + + await kv.hmset(`user:${email}`, user) + + return { + type: 'success', + resultCode: ResultCode.UserCreated + } + } +} + +interface Result { + type: string + resultCode: ResultCode +} export async function signup( - _prevState: AuthResult | undefined, + _prevState: Result | undefined, formData: FormData -) { +): Promise { const email = formData.get('email') as string const password = formData.get('password') as string @@ -34,42 +69,43 @@ export async function signup( ) const hashedPassword = getStringFromBuffer(hashedPasswordBuffer) - const client = await db.connect() - try { - await client.sql` - INSERT INTO users (email, password, salt) - VALUES (${email}, ${hashedPassword}, ${salt}) - ON CONFLICT (id) DO NOTHING; - ` + const result = await createUser(email, hashedPassword, salt) - await signIn('credentials', { - email, - password, - redirect: false - }) + if (result.resultCode === ResultCode.UserCreated) { + await signIn('credentials', { + email, + password, + redirect: false + }) + } - return { type: 'success', message: 'Account created!' } + return result } catch (error) { - const { message } = error as Error - - if ( - message.startsWith('duplicate key value violates unique constraint') - ) { - return { type: 'error', message: 'User already exists! Please log in.' } + if (error instanceof AuthError) { + switch (error.type) { + case 'CredentialsSignin': + return { + type: 'error', + resultCode: ResultCode.InvalidCredentials + } + default: + return { + type: 'error', + resultCode: ResultCode.UnknownError + } + } } else { return { type: 'error', - message: 'Something went wrong! Please try again.' + resultCode: ResultCode.UnknownError } } - } finally { - client.release() } } else { return { type: 'error', - message: 'Invalid entries, please try again!' + resultCode: ResultCode.InvalidCredentials } } } diff --git a/auth.ts b/auth.ts index 882cc95..7542992 100644 --- a/auth.ts +++ b/auth.ts @@ -2,25 +2,8 @@ import NextAuth from 'next-auth' import Credentials from 'next-auth/providers/credentials' import { authConfig } from './auth.config' import { z } from 'zod' -import { sql } from '@vercel/postgres' import { getStringFromBuffer } from './lib/utils' - -interface User { - id: string - name: string - email: string - password: string - salt: string -} - -async function getUser(email: string): Promise { - try { - const user = await sql`SELECT * FROM users WHERE email=${email}` - return user.rows[0] - } catch (error) { - throw new Error('Failed to fetch user.') - } -} +import { getUser } from './app/login/actions' export const { auth, signIn, signOut } = NextAuth({ ...authConfig, diff --git a/components/login-form.tsx b/components/login-form.tsx index a9269af..0a6910d 100644 --- a/components/login-form.tsx +++ b/components/login-form.tsx @@ -6,26 +6,30 @@ import Link from 'next/link' import { useEffect } from 'react' import { toast } from 'sonner' import { IconSpinner } from './ui/icons' +import { getMessageFromCode } from '@/lib/utils' +import { useRouter } from 'next/navigation' export default function LoginForm() { + const router = useRouter() const [result, dispatch] = useFormState(authenticate, undefined) useEffect(() => { if (result) { if (result.type === 'error') { - toast.error(result.message) + toast.error(getMessageFromCode(result.resultCode)) } else { - toast.success(result.message) + toast.success(getMessageFromCode(result.resultCode)) + router.refresh() } } - }, [result]) + }, [result, router]) return (
-
+

Please log in to continue.

@@ -84,7 +88,7 @@ function LoginButton() { return (