### 注册
POST http://localhost:3000/api/signup
Content-Type: application/json{"username":"admin1","password":"123"}
### 注册失败: 用户已存在
POST http://localhost:3000/api/signup
Content-Type: application/json
{"username":"admin"}
2. Check In Sign Up Endpoint
3. Sign Up Page
'use client'
// app/(public)/signup/form.tsx
import React, {FormEvent, useState} from "react";
function Form() {
const [username, setUsername] =
useState('')
const [password, setPassword] =
useState('')
const [confirmPassword, setConfirmPassword] =
useState('')
const [errors, setErrors] = useState([])
async function handleSubmit(e: FormEvent) {
e.preventDefault()
setErrors([])
if (password != confirmPassword) {
const newErrs = []
newErrs.push("Passwords do not match.")
setErrors(newErrs)
return
}
const res = await fetch('/api/signup', {
method: 'post',
body: JSON.stringify({username, password})
})
if (res.ok) {
window.location.href = '/signin'
} else {
alert('sign up failed')
}
}
return (
)
}
export default Form
// app/(public)/signup/page.tsx
import Form from "@/app/(public)/signup/form";
export default async function SignUp() {
return (
)
}
4. Check In Sign Up Page
5. Set Up Production DB
7. Confirm Password Error
'use client'
// app/(public)/signup/form.tsx
import React, {FormEvent, useState} from "react";
function Form() {
// ...
return (
)
}
export default Form
8. Env Example File
8. Authentication and Private Layout
1. Private Layout
// app/(private)/footer.tsx
export default function Footer() {
return (
)
}
// app/(private)/header.tsx
export default function Header() {
return (
Header
)
}
// app/(private)/navbar.tsx
export default function NavBar() {
return (
)
}
// app/(private)/layout.tsx
import React from "react";
import Footer from "@/app/(private)/footer";
import Header from "@/app/(private)/header";
import NavBar from "@/app/(private)/navbar";
export default function PrivateLayout(
{
children,
}: {
children: React.ReactNode
}
) {
return (
{children}
)
}
2. JWT Verification
// app/api/users/profile/route.ts
import {getJWTPayload} from "@/app/util/auth";
import {NextResponse} from "next/server";
import {sql} from "@/db";
export async function GET(request: Request) {
// get currently logged in user
const jwtPayload = await getJWTPayload()
// fetch user data
const res = await sql(
"select id, username, avatar from users where id=$1",
[jwtPayload.sub]
)
const user = res.rows[0]
// return user data
return NextResponse.json({data: user})
}
// app/util/auth.ts
import {cookies} from "next/headers";
import {jwtVerify} from "jose";
export async function getJWTPayload() {
const cookieStore = cookies()
const token = cookieStore.get("jwt-token")
const secret = new TextEncoder().encode(process.env.JWT_SECRET!)
// @ts-ignore
const {payload, protectedHeader} = await jwtVerify(token?.value, secret)
return payload
}
// app/(private)/header.tsx
'use client' // useSWR要客户端组件才能用, 否则报错useSWR is not a function
import useSWR from "swr";
const fetcher = async (url: RequestInfo | URL) => {
const res = await fetch(url)
if (!res.ok) {
const info = await res.json()
const status = res.status
console.error(info, status)
const msg = "An error occurred while fetching the data."
throw new Error(msg)
}
return res.json()
}
export default function Header() {
// 请求/api/users/profile, 如果成功就返回data, 失败返回error
const {data, error, isLoading} = useSWR('/api/users/profile', fetcher)
if (error) return
failed to load
if (isLoading) return
loading...
return (
{data.data.username}
)
}
7. Refactor Fetcher
// app/(private)/header.tsx
'use client' // useSWR要客户端组件才能用, 否则报错useSWR is not a function
import useSWR from "swr";
export default function Header() {
// 请求/api/users/profile, 如果成功就返回data, 失败返回error
const {data, error, isLoading} = useSWR('/api/users/profile')
// ...
}
// app/(private)/layout.tsx
import React from "react";
import Footer from "@/app/(private)/footer";
import Header from "@/app/(private)/header";
import NavBar from "@/app/(private)/navbar";
import {SWRConfig} from "swr";
import fetcher from "@/app/util/fetcher";
export default function PrivateLayout(
{
children,
}: {
children: React.ReactNode
}
) {
return (
// 配置后子元素swr使用不需要再导入fetcher
{children}
)
}
// app/util/fetcher.tsconst fetcher = async (url: RequestInfo | URL)=>{const res = await fetch(url)if(!res.ok){const info = await res.json()const status = res.status
console.error(info, status)const msg ="An error occurred while fetching the data."thrownewError(msg)}return res.json()}exportdefault fetcher
8. SWR Must Use Client
// app/(private)/layout.tsx
'use client'
// ...
9. Check In SWR Config
10. Styling the Header
// app/(private)/header.tsx
'use client' // useSWR要客户端组件才能用, 否则报错useSWR is not a function
import useSWR from "swr";
export default function Header() {
// 请求/api/users/profile, 如果成功就返回data, 失败返回error
const {data, error, isLoading} = useSWR('/api/users/profile')
if (error) return
failed to load
if (isLoading) return
loading...
return (
Strings
)
}
// app/components/user.tsx
function User({user, href}: { user: UserI, href?: string }) {
return (
)
}
export default User
11. Display User Avatar
// app/components/user.tsx
import {UserI} from "@/app/types";
import Link from "next/link";
import React from "react";
import Image from 'next/image'
function User({user, href}: { user: UserI, href?: string }) {
return (
// app/(private)/header.tsx
'use client' // useSWR要客户端组件才能用, 否则报错useSWR is not a function
import useSWR from "swr";
import User from "@/app/components/user";
export default function Header() {
// ...
return (
Strings
)
}
13. Center the Private Layout
// app/(private)/layout.tsx
'use client'
import React from "react";
import Footer from "@/app/(private)/footer";
import Header from "@/app/(private)/header";
import NavBar from "@/app/(private)/navbar";
import {SWRConfig} from "swr";
import fetcher from "@/app/util/fetcher";
export default function PrivateLayout(
{
children,
}: {
children: React.ReactNode
}
) {
return (
// 配置后子元素swr使用不需要再导入fetcher
{children}
)
}
14. NavBar and Footer
// app/(private)/navbar.tsx
import Link from "next/link";
export default function NavBar() {
return (
)
}