app actions.ts
本地来源:模版/template/raphael-starterkit-v1-main/analysis/02-应用核心文件/app-actions.ts.md
app/actions.ts 详细分析
文件作用
app/actions.ts 是 Next.js App Router 的服务器动作文件,包含了用户认证、支付等服务器端操作函数。服务器动作是 Next.js 13+ 的新特性,允许在客户端直接调用服务器端函数。
逐行代码分析
"use server";
第1行: 服务器动作指令
- "use server" 是 Next.js 的指令,标记整个文件为服务器动作
- 这个文件中的所有函数都会在服务器端执行
- 可以直接在客户端组件中调用这些函数
第2行: 空行
import { encodedRedirect } from "@/utils/utils";
第3行: 导入重定向工具函数
- encodedRedirect 是自定义的重定向函数
- 可以在重定向时携带错误或成功消息
- 使用 URL 编码确保消息的安全传输
import { createClient } from "@/utils/supabase/server";
第4行: 导入 Supabase 服务器客户端
- createClient 创建服务器端的 Supabase 客户端
- 可以安全地访问服务器端 API 和数据库
- 在服务器动作中执行认证和数据操作
import { headers } from "next/headers";
第5行: 导入 Next.js 头部信息
- headers 函数获取当前请求的头部信息
- 在服务器端可以访问 origin、user-agent 等信息
- 用于生成回调 URL 等
import { redirect } from "next/navigation";
第6行: 导入 Next.js 重定向函数
- redirect 是 Next.js 的重定向函数
- 在服务器动作中用于导航到其他页面
- 支持绝对和相对 URL
第7行: 空行
export const signUpAction = async (formData: FormData) => {
第8行: 导出注册动作函数
- export const 导出常量函数
- signUpAction 处理用户注册
- async 异步函数,可以等待异步操作
- formData: FormData 参数类型,来自表单提交
const email = formData.get("email")?.toString();
第9行: 获取邮箱地址
- formData.get("email") 从表单数据中获取邮箱字段
- ?.toString() 可选链调用,安全地转换为字符串
- 如果字段不存在,返回 undefined
const password = formData.get("password")?.toString();
第10行: 获取密码 - 类似地从表单数据中获取密码字段 - 使用可选链确保安全的类型转换
const supabase = await createClient();
第11行: 创建 Supabase 客户端
- await createClient() 异步创建客户端实例
- 在服务器端安全地访问 Supabase 服务
const origin = (await headers()).get("origin");
第12行: 获取请求来源
- await headers() 获取请求头信息
- .get("origin") 获取请求的来源域名
- 用于构建回调 URL
第13行: 空行
if (!email || !password) {
第14行: 验证输入数据
- 检查邮箱和密码是否都存在
- !email || !password 逻辑或运算,任一为空则为真
return encodedRedirect(
第15行: 返回错误重定向
- 调用 encodedRedirect 函数进行重定向
- 这是一个错误情况的处理
"error",
第16行: 错误类型
- "error" 表示这是一个错误消息
- 会在目标页面显示为错误提示
"/sign-up",
第17行: 重定向目标
- "/sign-up" 重定向回注册页面
- 让用户可以重新填写表单
"Email and password are required"
第18行: 错误消息 - 提示用户邮箱和密码都是必需的 - 这个消息会显示在注册页面
);
第19行: 函数调用结束
}
第20行: 验证条件结束
第21行: 空行
const { error } = await supabase.auth.signUp({
第22行: 调用 Supabase 注册API
- await supabase.auth.signUp() 异步调用注册功能
- 解构赋值只获取 error 字段
- 如果注册成功,error 为 null
email,
第23行: 传递邮箱参数
- 使用 ES6 简写语法,等同于 email: email
password,
第24行: 传递密码参数 - 同样使用简写语法
options: {
第25行: 注册选项对象
- options 包含额外的注册配置
emailRedirectTo: `${origin}/auth/callback`,
第26行: 邮件重定向URL
- emailRedirectTo 设置邮件确认后的重定向地址
- ${origin}/auth/callback 使用模板字符串构建完整URL
- 用户点击邮件中的链接后会跳转到这个地址
},
第27行: 选项对象结束
});
第28行: 注册API调用结束
第29行: 空行
if (error) {
第30行: 检查是否有错误 - 如果注册过程中出现错误
console.error(error.code + " " + error.message);
第31行: 记录错误信息
- console.error 在服务器控制台记录错误
- 拼接错误代码和错误消息
return encodedRedirect("error", "/sign-up", error.message);
第32行: 返回错误重定向
- 重定向到注册页面并显示具体的错误消息
- error.message 是 Supabase 返回的错误信息
} else {
第33行: 注册成功的情况
return encodedRedirect("success", "/dashboard", "Thanks for signing up!");
第34行: 返回成功重定向
- "success" 表示成功消息
- 重定向到 /dashboard 仪表板页面
- 显示感谢注册的消息
}
第35行: 条件语句结束
};
第36行: 注册函数结束
第37行: 空行
export const signInAction = async (formData: FormData) => {
第38行: 导出登录动作函数 - 处理用户登录的服务器动作
const email = formData.get("email") as string;
第39行: 获取邮箱(类型断言)
- 使用 as string 进行类型断言
- 确信这里的值一定是字符串类型
const password = formData.get("password") as string;
第40行: 获取密码(类型断言)
const supabase = await createClient();
第41行: 创建 Supabase 客户端
第42行: 空行
const { error } = await supabase.auth.signInWithPassword({
第43行: 调用密码登录API
- signInWithPassword 使用邮箱密码登录
email,
第44行: 传递邮箱参数
password,
第45行: 传递密码参数
});
第46行: 登录API调用结束
第47行: 空行
if (error) {
第48行: 检查登录错误
return encodedRedirect("error", "/sign-in", error.message);
第49行: 返回登录错误 - 重定向回登录页面并显示错误消息
}
第50行: 错误处理结束
第51行: 空行
return redirect("/dashboard");
第52行: 登录成功重定向 - 直接重定向到仪表板,不需要显示消息
};
第53行: 登录函数结束
第54行: 空行
export const forgotPasswordAction = async (formData: FormData) => {
第55行: 导出忘记密码动作函数
const email = formData.get("email")?.toString();
第56行: 获取邮箱地址
const supabase = await createClient();
第57行: 创建客户端
const origin = (await headers()).get("origin");
第58行: 获取请求来源
const callbackUrl = formData.get("callbackUrl")?.toString();
第59行: 获取回调URL - 可选的回调地址,用于自定义重定向
第60行: 空行
if (!email) {
第61行: 验证邮箱是否存在
return encodedRedirect("error", "/forgot-password", "Email is required");
第62行: 邮箱为空的错误处理
}
第63行: 验证结束
第64行: 空行
const { error } = await supabase.auth.resetPasswordForEmail(email, {
第65行: 调用重置密码API
- resetPasswordForEmail 发送重置密码邮件
redirectTo: `${origin}/auth/callback?redirect_to=/dashboard/reset-password`,
第66行: 设置邮件重定向 - 用户点击邮件链接后的跳转地址 - 包含一个查询参数指定最终目标页面
});
第67行: API调用结束
第68行: 空行
if (error) {
第69行: 检查重置错误
console.error(error.message);
第70行: 记录错误
return encodedRedirect(
第71行: 返回错误重定向
"error",
第72行: 错误类型
"/forgot-password",
第73行: 重定向目标
"Could not reset password"
第74行: 错误消息
);
第75行: 重定向调用结束
}
第76行: 错误处理结束
第77行: 空行
if (callbackUrl) {
第78行: 检查是否有自定义回调URL
return redirect(callbackUrl);
第79行: 重定向到自定义URL
}
第80行: 条件结束
第81行: 空行
return encodedRedirect(
第82行: 返回成功重定向
"success",
第83行: 成功类型
"/forgot-password",
第84行: 重定向目标
"Check your email for a link to reset your password."
第85行: 成功消息 - 提示用户检查邮件
);
第86行: 重定向调用结束
};
第87行: 忘记密码函数结束
第88行: 空行
export const resetPasswordAction = async (formData: FormData) => {
第89行: 导出重置密码动作函数
const supabase = await createClient();
第90行: 创建客户端
第91行: 空行
const password = formData.get("password") as string;
第92行: 获取新密码
const confirmPassword = formData.get("confirmPassword") as string;
第93行: 获取确认密码
第94行: 空行
if (!password || !confirmPassword) {
第95行: 验证密码字段 - 检查两个密码字段都存在
encodedRedirect(
第96行: 密码为空的错误处理
- 注意:这里缺少 return,可能是代码错误
"error",
第97行: 错误类型
"/dashboard/reset-password",
第98行: 重定向目标
"Password and confirm password are required"
第99行: 错误消息
);
第100行: 重定向调用结束
}
第101行: 验证结束
第102行: 空行
if (password !== confirmPassword) {
第103行: 验证密码一致性
encodedRedirect(
第104行: 密码不匹配的错误处理
- 同样缺少 return
"error",
第105行: 错误类型
"/dashboard/reset-password",
第106行: 重定向目标
"Passwords do not match"
第107行: 错误消息
);
第108行: 重定向调用结束
}
第109行: 验证结束
第110行: 空行
const { error } = await supabase.auth.updateUser({
第111行: 调用更新用户API
- updateUser 更新用户信息,包括密码
password: password,
第112行: 传递新密码
});
第113行: API调用结束
第114行: 空行
if (error) {
第115行: 检查更新错误
encodedRedirect(
第116行: 更新失败的错误处理
- 同样缺少 return
"error",
第117行: 错误类型
"/dashboard/reset-password",
第118行: 重定向目标
"Password update failed"
第119行: 错误消息
);
第120行: 重定向调用结束
}
第121行: 错误处理结束
第122行: 空行
encodedRedirect("success", "/dashboard/reset-password", "Password updated");
第123行: 成功重定向
- 密码更新成功的处理
- 同样缺少 return
};
第124行: 重置密码函数结束
第125行: 空行
export const signOutAction = async () => {
第126行: 导出登出动作函数 - 不需要参数的简单登出函数
const supabase = await createClient();
第127行: 创建客户端
await supabase.auth.signOut();
第128行: 调用登出API - 清除用户会话
return redirect("/sign-in");
第129行: 重定向到登录页 - 登出后回到登录页面
};
第130行: 登出函数结束
第131行: 空行
export async function createCheckoutSession(
第132行: 导出创建支付会话函数
- 使用 function 关键字而不是箭头函数
productId: string,
第133行: 产品ID参数
email: string,
第134行: 用户邮箱参数
userId: string,
第135行: 用户ID参数
productType: "subscription" | "credits",
第136行: 产品类型参数 - 联合类型,只能是订阅或积分
credits_amount?: number,
第137行: 积分数量(可选) - 当产品类型是积分时使用
discountCode?: string
第138行: 折扣码(可选)
) {
第139行: 函数参数结束
try {
第140行: 异常处理开始
const requestBody: any = {
第141行: 构建请求体
- 使用 any 类型(不是最佳实践)
product_id: productId,
第142行: 设置产品ID
// request_id: `${userId}-${Date.now()}`, // use Unique request ID if you need
第143行: 注释的请求ID - 可选的唯一请求标识符
customer: {
第144行: 客户信息对象
email: email,
第145行: 客户邮箱
},
第146行: 客户信息结束
metadata: {
第147行: 元数据对象 - 存储额外的信息
user_id: userId,
第148行: 用户ID元数据
product_type: productType,
第149行: 产品类型元数据
credits: credits_amount || 0,
第150行: 积分数量元数据 - 如果没有提供则默认为0
},
第151行: 元数据结束
};
第152行: 请求体构建结束
第153行: 空行
// 如果配置了成功重定向 URL,则添加到请求中
第154行: 中文注释说明
if (process.env.CREEM_SUCCESS_URL) {
第155行: 检查环境变量 - 如果配置了成功URL
requestBody.success_url = process.env.CREEM_SUCCESS_URL;
第156行: 添加成功URL到请求体
}
第157行: 条件结束
第158行: 空行
// 添加折扣码(如果有)
第159行: 中文注释
if (discountCode) {
第160行: 检查折扣码
requestBody.discount_code = discountCode;
第161行: 添加折扣码到请求体
}
第162行: 条件结束
第163行: 空行
const response = await fetch(process.env.CREEM_API_URL + "/checkouts", {
第164行: 发送HTTP请求 - 调用 Creem API 创建支付会话
method: "POST",
第165行: 设置请求方法
headers: {
第166行: 设置请求头
"x-api-key": process.env.CREEM_API_KEY!,
第167行: API密钥头
- 使用非空断言操作符 !
"Content-Type": "application/json",
第168行: 内容类型头
},
第169行: 请求头结束
body: JSON.stringify(requestBody),
第170行: 请求体 - 将对象序列化为JSON字符串
});
第171行: 请求配置结束
第172行: 空行
if (!response.ok) {
第173行: 检查响应状态
throw new Error("Failed to create checkout session");
第174行: 抛出错误 - 如果响应不成功
}
第175行: 状态检查结束
第176行: 空行
const data = await response.json();
第177行: 解析响应JSON
return data.checkout_url;
第178行: 返回支付URL - 返回 Creem 提供的支付页面URL
} catch (error) {
第179行: 异常捕获
console.error("Error creating checkout session:", error);
第180行: 记录错误
throw error;
第181行: 重新抛出错误 - 让调用者处理错误
}
第182行: 异常处理结束
}
第183行: 函数结束
重要概念解释
1. 服务器动作 (Server Actions)
- Next.js 13+ 的新特性
- 在服务器端执行,客户端直接调用
- 自动处理序列化和网络传输
- 比传统 API 路由更简洁
2. FormData 处理
- 接收来自 HTML 表单的数据
- 可以包含文件上传
- 需要手动提取和验证字段
3. 错误处理模式
- 使用
encodedRedirect在URL中传递消息 - 区分错误和成功消息
- 提供用户友好的反馈
4. Supabase 认证流程
- 注册需要邮件验证
- 支持密码重置
- 自动处理会话管理
代码质量问题
1. 缺少 return 语句
在 resetPasswordAction 函数中,多处 encodedRedirect 调用缺少 return,这可能导致函数继续执行。
2. 类型安全
- 使用了
any类型 - 可以使用更具体的接口定义
3. 改进建议
// 更好的类型定义
interface CheckoutRequest {
product_id: string;
customer: {
email: string;
};
metadata: {
user_id: string;
product_type: "subscription" | "credits";
credits: number;
};
success_url?: string;
discount_code?: string;
}
总结
这个文件展示了 Next.js 服务器动作的强大功能:
- 统一的认证处理 - 注册、登录、密码重置
- 支付集成 - 与 Creem API 的完整集成
- 错误处理 - 统一的错误消息和重定向
- 类型安全 - 使用 TypeScript 类型检查
- 服务器端安全 - 所有敏感操作在服务器端执行
这是现代全栈 React 应用的典型实现,将前端和后端逻辑无缝结合。
本文档为站内渲染。原始文件本地路径:saas/source/templates/模版-template-raphael-starterkit-v1-main-analysis-02-应用核心文件-ap-713af0.md(仅本地保留,不入库不部署)