redirect on login now works
This commit is contained in:
		
							parent
							
								
									8a04297768
								
							
						
					
					
						commit
						479c42350f
					
				
							
								
								
									
										
											BIN
										
									
								
								prisma/dev.db
								
								
								
								
							
							
						
						
									
										
											BIN
										
									
								
								prisma/dev.db
								
								
								
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -6,28 +6,21 @@ import { loginSchema, LoginSchema } from 'app/login/schema';
 | 
			
		|||
import { NextResponse } from 'next/server';
 | 
			
		||||
 | 
			
		||||
export async function getJWTSecretKey<Uint8Array>() {
 | 
			
		||||
	console.log('getJWTSecretKey called!')
 | 
			
		||||
	const secret = process.env.JWT_SECRET
 | 
			
		||||
	console.log('secret: ' + secret)
 | 
			
		||||
	if (!secret) throw new Error("There is no JWT secret key")
 | 
			
		||||
	console.log('encoding...')
 | 
			
		||||
	try {
 | 
			
		||||
		const enc: Uint8Array = new TextEncoder().encode(secret)
 | 
			
		||||
		console.log('enc')
 | 
			
		||||
		return enc
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		console.error('aw shit: ' + error.message)
 | 
			
		||||
		throw new Error("failed to getJWTSecretKey", error.message)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function verifyJwt(token: string): Promise<JWTPayload | null> {
 | 
			
		||||
	console.log('verifyJwt called for token: ' + token)
 | 
			
		||||
	const key = await getJWTSecretKey()
 | 
			
		||||
	console.log('key: ' + key)
 | 
			
		||||
	const { payload } = await jwtVerify(token, key)
 | 
			
		||||
	console.log('payload: ' + payload)
 | 
			
		||||
	return payload
 | 
			
		||||
	try {
 | 
			
		||||
		const key = await getJWTSecretKey()
 | 
			
		||||
		const { payload } = await jwtVerify(token, key)
 | 
			
		||||
		return payload
 | 
			
		||||
	} catch {
 | 
			
		||||
		return null
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,24 +3,39 @@ import { Genre, Story } from "@prisma/client"
 | 
			
		|||
import prisma from "./db"
 | 
			
		||||
import { revalidatePath } from "next/cache"
 | 
			
		||||
import { redirect } from "next/navigation"
 | 
			
		||||
import { z } from "zod"
 | 
			
		||||
import { storySchema } from "app/ui/forms/schemas"
 | 
			
		||||
 | 
			
		||||
export async function createStory(data: Story & { genres: number[] }) {
 | 
			
		||||
//TODO - data validation, error handling, unauthorized access handling
 | 
			
		||||
 | 
			
		||||
export async function createStory(data: Story & { genres: number[] }): Promise<Story> | undefined {
 | 
			
		||||
	// will return undefined if middleware authorization fails
 | 
			
		||||
	"use server"
 | 
			
		||||
	//Prepare data
 | 
			
		||||
	const genresArray = data.genres.map((e) => { return { id: e } })
 | 
			
		||||
	const res = await prisma.story.create({
 | 
			
		||||
		data: {
 | 
			
		||||
			title: data.title,
 | 
			
		||||
			word_count: data.word_count,
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	console.log(res)
 | 
			
		||||
	const genresRes = await prisma.story.update({
 | 
			
		||||
		where: { id: res.id },
 | 
			
		||||
		data: {
 | 
			
		||||
			genres: { set: genresArray }
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	console.log(genresRes)
 | 
			
		||||
	const storyData = {
 | 
			
		||||
		title: data.title,
 | 
			
		||||
		word_count: data.word_count,
 | 
			
		||||
	}
 | 
			
		||||
	//prepare schemas
 | 
			
		||||
	const schema = storySchema.omit({ id: true, genres: true })
 | 
			
		||||
	const genreSchema = z.object({ id: z.number() })
 | 
			
		||||
	const genresSchema = z.array(genreSchema)
 | 
			
		||||
	//validate
 | 
			
		||||
	const isSafe = schema.safeParse(storyData) && genresSchema.safeParse(genresArray)
 | 
			
		||||
	if (!isSafe) throw new Error("server-side validation failed")
 | 
			
		||||
	//submit
 | 
			
		||||
	try {
 | 
			
		||||
		const res = await prisma.story.create({ data: storyData })
 | 
			
		||||
		await prisma.story.update({
 | 
			
		||||
			where: { id: res.id },
 | 
			
		||||
			data: {
 | 
			
		||||
				genres: { set: genresArray }
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		throw new Error("database failure")
 | 
			
		||||
	}
 | 
			
		||||
	revalidatePath("/story")
 | 
			
		||||
	redirect("/story")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,22 +9,22 @@ import { Button } from "@/components/ui/button";
 | 
			
		|||
import { redirect } from "next/navigation";
 | 
			
		||||
import { loginSchema } from "./schema";
 | 
			
		||||
import { useRouter } from "next/navigation";
 | 
			
		||||
 | 
			
		||||
import { useSearchParams } from "next/navigation";
 | 
			
		||||
import revalidate from "./revalidate";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
 | 
			
		||||
export default function LoginForm() {
 | 
			
		||||
	const router = useRouter()
 | 
			
		||||
	const searchParams = useSearchParams()
 | 
			
		||||
	const redirect = searchParams.get("from")
 | 
			
		||||
	const form = useForm<z.infer<typeof loginSchema>>({
 | 
			
		||||
		resolver: zodResolver(loginSchema),
 | 
			
		||||
	})
 | 
			
		||||
	const [submitted, setSubmitted] = useState(false)
 | 
			
		||||
 | 
			
		||||
	const onSubmit = form.handleSubmit(async (data) => {
 | 
			
		||||
		// const res = await login(data)
 | 
			
		||||
		// if (res?.error) {
 | 
			
		||||
		// 	toast({ title: "Whoops!", description: res.error })
 | 
			
		||||
		// 	form.reset()
 | 
			
		||||
		// } else {
 | 
			
		||||
		// 	toast({ title: "login successful" })
 | 
			
		||||
		// }
 | 
			
		||||
	const onSubmit = form.handleSubmit(async (data, event) => {
 | 
			
		||||
		event.preventDefault()
 | 
			
		||||
		const res = await fetch('/api/auth/login', {
 | 
			
		||||
			method: 'POST',
 | 
			
		||||
			headers: {
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +34,10 @@ export default function LoginForm() {
 | 
			
		|||
		})
 | 
			
		||||
		if (res.status === 200) {
 | 
			
		||||
			toast({ title: "login successful!" })
 | 
			
		||||
			router.push('/submission')
 | 
			
		||||
			setSubmitted(true)
 | 
			
		||||
			await revalidate(redirect)
 | 
			
		||||
			router.refresh()
 | 
			
		||||
			router.push(redirect)
 | 
			
		||||
		} else {
 | 
			
		||||
			toast({ title: "login failed!" })
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -42,37 +45,41 @@ export default function LoginForm() {
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<Form {...form}>
 | 
			
		||||
			<form onSubmit={onSubmit}>
 | 
			
		||||
				<FormField
 | 
			
		||||
					control={form.control}
 | 
			
		||||
					name="email"
 | 
			
		||||
					render={({ field }) => (
 | 
			
		||||
						<FormItem>
 | 
			
		||||
							<FormLabel>Email Address</FormLabel>
 | 
			
		||||
							<FormControl>
 | 
			
		||||
								<Input placeholder="email goes here" {...field} />
 | 
			
		||||
							</FormControl>
 | 
			
		||||
							<FormMessage />
 | 
			
		||||
						</FormItem>
 | 
			
		||||
					)}
 | 
			
		||||
				></FormField>
 | 
			
		||||
				<FormField
 | 
			
		||||
					control={form.control}
 | 
			
		||||
					name="password"
 | 
			
		||||
					render={({ field }) => (
 | 
			
		||||
						<FormItem>
 | 
			
		||||
							<FormLabel>Password</FormLabel>
 | 
			
		||||
							<FormControl>
 | 
			
		||||
								<Input placeholder="password goes here" type="password"{...field} />
 | 
			
		||||
							</FormControl>
 | 
			
		||||
							<FormMessage />
 | 
			
		||||
						</FormItem>
 | 
			
		||||
					)}
 | 
			
		||||
				></FormField>
 | 
			
		||||
				<Button type="submit">SUBMIT</Button>
 | 
			
		||||
			</form>
 | 
			
		||||
		</Form>
 | 
			
		||||
		<>
 | 
			
		||||
			{submitted ? <p>Logging in...</p> :
 | 
			
		||||
				<Form {...form}>
 | 
			
		||||
					<form onSubmit={onSubmit}>
 | 
			
		||||
						<FormField
 | 
			
		||||
							control={form.control}
 | 
			
		||||
							name="email"
 | 
			
		||||
							render={({ field }) => (
 | 
			
		||||
								<FormItem>
 | 
			
		||||
									<FormLabel>Email Address</FormLabel>
 | 
			
		||||
									<FormControl>
 | 
			
		||||
										<Input placeholder="email goes here" {...field} />
 | 
			
		||||
									</FormControl>
 | 
			
		||||
									<FormMessage />
 | 
			
		||||
								</FormItem>
 | 
			
		||||
							)}
 | 
			
		||||
						></FormField>
 | 
			
		||||
						<FormField
 | 
			
		||||
							control={form.control}
 | 
			
		||||
							name="password"
 | 
			
		||||
							render={({ field }) => (
 | 
			
		||||
								<FormItem>
 | 
			
		||||
									<FormLabel>Password</FormLabel>
 | 
			
		||||
									<FormControl>
 | 
			
		||||
										<Input placeholder="password goes here" type="password"{...field} />
 | 
			
		||||
									</FormControl>
 | 
			
		||||
									<FormMessage />
 | 
			
		||||
								</FormItem>
 | 
			
		||||
							)}
 | 
			
		||||
						></FormField>
 | 
			
		||||
						<Button type="submit">SUBMIT</Button>
 | 
			
		||||
					</form>
 | 
			
		||||
				</Form>
 | 
			
		||||
			}
 | 
			
		||||
		</>
 | 
			
		||||
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
"use server"
 | 
			
		||||
import { revalidatePath } from "next/cache"
 | 
			
		||||
export default async function revalidate(path: string) {
 | 
			
		||||
	try {
 | 
			
		||||
		revalidatePath(path)
 | 
			
		||||
		return true
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		console.error(error)
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -16,18 +16,12 @@ import {
 | 
			
		|||
} from "@/components/ui/form"
 | 
			
		||||
import { Input } from "@/components/ui/input"
 | 
			
		||||
import { Checkbox } from "@/components/ui/checkbox"
 | 
			
		||||
 | 
			
		||||
const formSchema = z.object({
 | 
			
		||||
	title: z.string().min(2).max(50),
 | 
			
		||||
	word_count: z.number(),
 | 
			
		||||
	genres: z.object({ id: z.number(), name: z.string() }).array()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
import { storySchema } from "./schemas"
 | 
			
		||||
 | 
			
		||||
export default function FancyForm({ genres }) {
 | 
			
		||||
	// 1. Define your form.
 | 
			
		||||
	const form = useForm<z.infer<typeof formSchema>>({
 | 
			
		||||
		resolver: zodResolver(formSchema),
 | 
			
		||||
	const form = useForm<z.infer<typeof storySchema>>({
 | 
			
		||||
		resolver: zodResolver(storySchema),
 | 
			
		||||
		defaultValues: {
 | 
			
		||||
			title: "",
 | 
			
		||||
			word_count: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -35,7 +29,7 @@ export default function FancyForm({ genres }) {
 | 
			
		|||
		},
 | 
			
		||||
	})
 | 
			
		||||
	// 2. Define a submit handler.
 | 
			
		||||
	function onSubmit(values: z.infer<typeof formSchema>) {
 | 
			
		||||
	function onSubmit(values: z.infer<typeof storySchema>) {
 | 
			
		||||
		// Do something with the form values.
 | 
			
		||||
		// ✅ This will be type-safe and validated.
 | 
			
		||||
		console.log(values)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import { z } from "zod";
 | 
			
		||||
 | 
			
		||||
export const storySchema = z.object({
 | 
			
		||||
	title: z.string().min(2).max(50),
 | 
			
		||||
	word_count: z.number(),
 | 
			
		||||
	genres: z.object({ id: z.number(), name: z.string() }).array()
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -40,16 +40,20 @@ export default function StoryForm({ genres, createStory, className, existingData
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	function onSubmit(values: z.infer<typeof formSchema>) {
 | 
			
		||||
		toast({
 | 
			
		||||
			title: "You submitted the following values:",
 | 
			
		||||
			description: (
 | 
			
		||||
				<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
 | 
			
		||||
					<code className="text-white">{JSON.stringify(values, null, 2)}</code>
 | 
			
		||||
				</pre>
 | 
			
		||||
			),
 | 
			
		||||
		})
 | 
			
		||||
		createStory(values)
 | 
			
		||||
	async function onSubmit(values: z.infer<typeof formSchema>) {
 | 
			
		||||
		try {
 | 
			
		||||
			await createStory(values)
 | 
			
		||||
			toast({
 | 
			
		||||
				title: "Successfully submitted:",
 | 
			
		||||
				description: values.title,
 | 
			
		||||
			})
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			toast({
 | 
			
		||||
				title: "UH-OH",
 | 
			
		||||
				description: error.message
 | 
			
		||||
			})
 | 
			
		||||
			console.error(error)
 | 
			
		||||
		}
 | 
			
		||||
		console.log(values)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,7 +24,7 @@ export default function GenrePickerInputCell(props: CellContext<any, any>) {
 | 
			
		|||
  const genres = props.table.options.meta.genres
 | 
			
		||||
  const [isActive, setIsActive] = useState(false)
 | 
			
		||||
 | 
			
		||||
  async function onSubmit({ genres }: { genres: number[] }) {
 | 
			
		||||
  async function onSubmit({ genres }: { genres: number[] }, event: Event) {
 | 
			
		||||
    event.preventDefault()
 | 
			
		||||
    const genresArray = genres.map((e) => { return { id: e } })
 | 
			
		||||
    console.log(`genres: ${genres}, genresArray: ${JSON.stringify(genresArray)}`)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,15 +13,18 @@ function matchesWildcard(path: string, pattern: string): boolean {
 | 
			
		|||
	return path === pattern;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async function(request: NextRequest) {
 | 
			
		||||
	// const url = `${process.env.NEXT_PUBLIC_BASE_URL}/login?redirect=${request.nextUrl.pathname + request.nextUrl.search}`
 | 
			
		||||
export default async function(request: NextRequest): Promise<NextResponse> | undefined {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	const url = request.nextUrl.clone()
 | 
			
		||||
	url.pathname = "/login"
 | 
			
		||||
	url.searchParams.set('from', request.nextUrl.pathname)
 | 
			
		||||
	if (protectedRoutes.some(pattern => matchesWildcard(request.nextUrl.pathname, pattern))) {
 | 
			
		||||
		const token = request.cookies.get('token')
 | 
			
		||||
		//NOTE - may need to add logic to return 401 for api routes
 | 
			
		||||
 | 
			
		||||
		if (!token) {
 | 
			
		||||
			console.log("there is no jwt")
 | 
			
		||||
			return NextResponse.redirect(url)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,12 +37,14 @@ export default async function(request: NextRequest) {
 | 
			
		|||
				request.cookies.delete('token')
 | 
			
		||||
				return NextResponse.redirect(url)
 | 
			
		||||
			}
 | 
			
		||||
		} catch {
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			//delete token (failsafe)
 | 
			
		||||
			console.error("failed to very jwt", error.message)
 | 
			
		||||
			request.cookies.delete('token')
 | 
			
		||||
			return NextResponse.redirect(url)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//TODO - TEST THIS BECAUSE IT PROBABLY DOESN'T WORK
 | 
			
		||||
		//redirect from login if already logged in
 | 
			
		||||
		let redirectToApp = false
 | 
			
		||||
		if (request.nextUrl.pathname === "/login") {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue