[Next.js][App router]分享使用自幹auth模組的元件及Server Action
因為NextAuth無法直接在Firebase hosting上使用所以開始自幹一個auth模組,這篇分享一些關於模組週邊使用的5項程式碼。
分享以下5類程式碼
- AuthSessionProvider.tsx
- layout.tsx
- middleware.tsx
- Server Actions
- Components
1. providers/AuthSessionProvider.tsx
'use client';
import { createContext, useContext, useState, useEffect, useMemo } from 'react';
import type { ReactNode } from 'react';
import type { Session } from '@/types/authTypes';
export type AuthSessionContextType = {
session: Session | null;
}
export interface AuthSessionProviderProps {
config: Session | null;
children: ReactNode;
}
const AuthSessionContext = createContext<AuthSessionContextType | null>(null)
export default function AuthSessionProvider({ config, children }: AuthSessionProviderProps) {
const [session, setSession] = useState<Session | null>(config);
useEffect(() => setSession(config), [config]);
const contextData = useMemo<AuthSessionContextType>(() => ({ session }), [session])
return (
<AuthSessionContext.Provider value={contextData} >
{children}
</AuthSessionContext.Provider>
)
}
export function useSession() {
const contextData = useContext(AuthSessionContext) as AuthSessionContextType;
// 確保 context 是空的
if (contextData == null) {
throw new Error('useSession must be used within a AuthSessionProvider');
}
return { user: contextData.session?.user };
}
和layout一起用就能在Client component上顯示一些登入用戶得個人資本資訊。
2. app/(member)/layout.tsx
import * as React from 'react';
import { auth } from '@/auth';
import AuthSessionProvider from '@/providers/AuthSessionProvider';
import { MainLayout } from './MainLayout';
type Props = {
children: React.ReactNode;
};
export default async function MemberLayout(props: Props) {
const session = await auth();
return (
<AuthSessionProvider config={session}>
<MainLayout>
{props.children}
</MainLayout>
</AuthSessionProvider>
);
}
在App router中,可以把function改aync function就能執行server側的auth函數,再把這個資訊帶給client側的元件。
3. middleware.tsx
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(req: NextRequest) {
if (req.nextUrl.pathname.startsWith(AUTH_API_PATH)) {
return NextResponse.next();
}
try {
if (!req.cookies.has(SESSION_COOKIE_NAME)) {
throw new Error('not found session');
}
// 驗證cookie,這裡省略
// 持有已授權的session, 正常進入
return NextResponse.next();
}
catch (error: any) {
// 未授權, 消除cookie
if (req.cookies.has(SESSION_COOKIE_NAME)) {
req.cookies.delete(SESSION_COOKIE_NAME);
}
if (req.nextUrl.pathname.startsWith('/api')) {
return NextResponse.json({
status: 'error',
message: 'UNAUTHORIZED',
}, { status: 401 });
}
return NextResponse.redirect(REDIRECT_LOGIN_PATH);
}
}
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*'],
}
透過middleware來限制未登入的使用者存取特定頁面
4. Server Actions
authActions.ts
'use server';
import { signIn, signOut } from '@/auth';
export async function onSignIn() {
await signIn();
}
export async function onLineSignIn() {
await signIn('line');
}
export async function onCredentialSignIn(_previousState: any, data: FormData) {
return await signIn('email', undefined, {
username: data.get('username')?.toString(),
password: data.get('password')?.toString(),
});
}
export async function onSignOut() {
await signOut();
}
這裡模仿next-auth的server action們
5. Components
使用Server Action打造的一些元件
5.1 登入按鈕
跳轉到登入頁
import { Tooltip, Button } from '@mui/material';
import { onSignIn } from '@/authActions';
export default function SignInButton() {
return (
<form action={onSignIn}>
<Tooltip title="登入">
<Button type='submit'>用戶登入</Button>
</Tooltip>
</form>
);
}
5.2 帳密登入
'use client';
import { Typography, TextField, Button, Stack } from '@mui/material';
import { useFormState, useFormStatus } from 'react-dom';
import { onCredentialSignIn } from '@/authActions';
export default function CredentialLoginForm() {
const [state, action] = useFormState(onCredentialSignIn, { message: '' });
const { pending } = useFormStatus();
return (
<Stack direction='column' gap={3} component='form' action={action}>
<Typography>{pending ? '登入中...' : ''}</Typography>
<Typography>{state?.message}</Typography>
<TextField
fullWidth required
name='username'
label='電子郵件'
autoComplete='email'
disabled={pending}
/>
<TextField
fullWidth required
name='password'
label='密碼'
autoComplete='current-password'
type='password'
disabled={pending}
/>
<Button
fullWidth
type='submit'
variant='contained'
disabled={pending}
>登入</Button>
</Stack>
);
}
文章開頭的登入頁圖片即有使用這個帳密登入的按鈕
5.3 Line登入按鈕
import Button from '@mui/material/Button';
import { onLineSignIn } from '@/authActions';
export default function LineLoginButton() {
return (
<form action={onLineSignIn}>
<Button type='submit'>LINE登入</Button>
</form>
);
}
文章開頭的登入頁圖片即有使用這個Line登入按鈕
5.4 登出按鈕
import Button from '@mui/material/Button';
import { onSignOut } from '@/authActions';
import { useFormState, useFormStatus } from 'react-dom';
export default function SignOutButton() {
const [_, action] = useFormState(onSignOut, null);
const { pending } = useFormStatus();
return (
<form action={action}>
<Button type='submit' disabled={pending}>登出</Button>
</form>
);
}
和layout, title和footer結合起來就可以變成下面的登出頁和個人選單這樣
留言