mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 09:14:11 +05:30
feat: add subscription push transfers
This commit is contained in:
@@ -59,6 +59,11 @@ const settingsSchema = z.object({
|
||||
inviteRewardEnabled: z.string().optional(),
|
||||
inviteRewardRate: z.coerce.number().min(0).max(100).optional(),
|
||||
inviteRewardCouponId: z.string().trim().optional(),
|
||||
subscriptionTransferEnabled: z.string().optional(),
|
||||
subscriptionTransferFee: z.coerce.number().min(0).max(100000).optional(),
|
||||
subscriptionTransferLimitPerCycle: z.coerce.number().int().min(0).max(100).optional(),
|
||||
subscriptionTransferMinRemainingDays: z.coerce.number().int().min(0).max(3650).optional(),
|
||||
subscriptionTransferMinRemainingTrafficGb: z.coerce.number().int().min(0).max(1000000).optional(),
|
||||
turnstileSiteKey: z.string().trim().optional(),
|
||||
turnstileSecretKey: z.string().trim().optional(),
|
||||
smtpEnabled: z.string().optional(),
|
||||
@@ -128,6 +133,7 @@ function booleanSettingData(field: BooleanSettingField, value: boolean) {
|
||||
subscriptionRiskAutoSuspend: { subscriptionRiskAutoSuspend: value },
|
||||
nodeAccessRiskEnabled: { nodeAccessRiskEnabled: value },
|
||||
inviteRewardEnabled: { inviteRewardEnabled: value },
|
||||
subscriptionTransferEnabled: { subscriptionTransferEnabled: value },
|
||||
smtpEnabled: { smtpEnabled: value },
|
||||
smtpSecure: { smtpSecure: value },
|
||||
}[field];
|
||||
@@ -243,6 +249,18 @@ function buildSettingsUpdate(parsed: z.infer<typeof settingsSchema>, current: Aw
|
||||
inviteRewardEnabled: optionalBoolean(parsed.inviteRewardEnabled, current.inviteRewardEnabled),
|
||||
inviteRewardRate: parsed.inviteRewardRate ?? Number(current.inviteRewardRate),
|
||||
inviteRewardCouponId: parsed.inviteRewardCouponId || null,
|
||||
subscriptionTransferEnabled: optionalBoolean(
|
||||
parsed.subscriptionTransferEnabled,
|
||||
current.subscriptionTransferEnabled,
|
||||
),
|
||||
subscriptionTransferFee:
|
||||
parsed.subscriptionTransferFee ?? Number(current.subscriptionTransferFee),
|
||||
subscriptionTransferLimitPerCycle:
|
||||
parsed.subscriptionTransferLimitPerCycle ?? current.subscriptionTransferLimitPerCycle,
|
||||
subscriptionTransferMinRemainingDays:
|
||||
parsed.subscriptionTransferMinRemainingDays ?? current.subscriptionTransferMinRemainingDays,
|
||||
subscriptionTransferMinRemainingTrafficGb:
|
||||
parsed.subscriptionTransferMinRemainingTrafficGb ?? current.subscriptionTransferMinRemainingTrafficGb,
|
||||
turnstileSiteKey,
|
||||
turnstileSecretKey,
|
||||
smtpEnabled,
|
||||
@@ -290,6 +308,7 @@ function revalidateSettingsViews() {
|
||||
revalidatePath("/dashboard");
|
||||
revalidatePath("/store");
|
||||
revalidatePath("/subscriptions");
|
||||
revalidatePath("/subscriptions/push");
|
||||
revalidatePath("/admin/nodes");
|
||||
revalidatePath("/account");
|
||||
revalidatePath("/support");
|
||||
|
||||
25
src/actions/admin/subscription-transfers.ts
Normal file
25
src/actions/admin/subscription-transfers.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { requireAdmin } from "@/lib/require-auth";
|
||||
import { actorFromSession } from "@/services/audit";
|
||||
import { deleteSubscriptionTransferAsAdmin } from "@/services/subscription-transfer";
|
||||
|
||||
const transferIdSchema = z.string().trim().min(1, "套餐 Push 记录不存在");
|
||||
|
||||
export async function deleteAdminSubscriptionTransfer(transferId: string) {
|
||||
const session = await requireAdmin();
|
||||
const id = transferIdSchema.parse(transferId);
|
||||
|
||||
const result = await deleteSubscriptionTransferAsAdmin({
|
||||
transferId: id,
|
||||
actor: actorFromSession(session),
|
||||
});
|
||||
|
||||
revalidatePath("/admin/subscription-transfers");
|
||||
revalidatePath("/subscriptions");
|
||||
revalidatePath("/subscriptions/push");
|
||||
|
||||
return result;
|
||||
}
|
||||
97
src/actions/user/subscription-transfer.ts
Normal file
97
src/actions/user/subscription-transfer.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { requireAuth } from "@/lib/require-auth";
|
||||
import { getErrorMessage } from "@/lib/errors";
|
||||
import { actorFromSession } from "@/services/audit";
|
||||
import {
|
||||
acceptSubscriptionTransfer,
|
||||
cancelSubscriptionTransfer,
|
||||
createSubscriptionTransfer,
|
||||
rejectSubscriptionTransfer,
|
||||
} from "@/services/subscription-transfer";
|
||||
|
||||
const createTransferSchema = z.object({
|
||||
subscriptionId: z.string().trim().min(1, "请选择要 Push 的套餐"),
|
||||
recipientEmail: z.string().trim().email("请输入正确的接收方邮箱"),
|
||||
password: z.string().min(1, "请输入当前账户密码"),
|
||||
feePayer: z.enum(["SENDER", "RECIPIENT"]),
|
||||
});
|
||||
|
||||
const transferIdSchema = z.string().trim().min(1, "套餐 Push 不存在");
|
||||
type UserTransferActionResult = { ok: true; id?: string } | { ok: false; error: string };
|
||||
|
||||
function revalidateTransferViews(subscriptionId?: string) {
|
||||
revalidatePath("/subscriptions");
|
||||
revalidatePath("/subscriptions/push");
|
||||
revalidatePath("/wallet");
|
||||
if (subscriptionId) revalidatePath(`/subscriptions/${subscriptionId}`);
|
||||
}
|
||||
|
||||
export async function createUserSubscriptionTransfer(formData: FormData): Promise<UserTransferActionResult> {
|
||||
try {
|
||||
const session = await requireAuth();
|
||||
const data = createTransferSchema.parse(Object.fromEntries(formData));
|
||||
const transfer = await createSubscriptionTransfer({
|
||||
senderId: session.user.id,
|
||||
recipientEmail: data.recipientEmail,
|
||||
subscriptionId: data.subscriptionId,
|
||||
password: data.password,
|
||||
feePayer: data.feePayer,
|
||||
actor: actorFromSession(session),
|
||||
});
|
||||
revalidateTransferViews(data.subscriptionId);
|
||||
return { ok: true, id: transfer.id };
|
||||
} catch (error) {
|
||||
return { ok: false, error: getErrorMessage(error, "发起套餐 Push 失败") };
|
||||
}
|
||||
}
|
||||
|
||||
export async function acceptUserSubscriptionTransfer(transferId: string): Promise<UserTransferActionResult> {
|
||||
try {
|
||||
const session = await requireAuth();
|
||||
const id = transferIdSchema.parse(transferId);
|
||||
const transfer = await acceptSubscriptionTransfer({
|
||||
transferId: id,
|
||||
recipientId: session.user.id,
|
||||
actor: actorFromSession(session),
|
||||
});
|
||||
revalidateTransferViews(transfer.subscriptionId);
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
return { ok: false, error: getErrorMessage(error, "接收套餐 Push 失败") };
|
||||
}
|
||||
}
|
||||
|
||||
export async function rejectUserSubscriptionTransfer(transferId: string): Promise<UserTransferActionResult> {
|
||||
try {
|
||||
const session = await requireAuth();
|
||||
const id = transferIdSchema.parse(transferId);
|
||||
const transfer = await rejectSubscriptionTransfer({
|
||||
transferId: id,
|
||||
recipientId: session.user.id,
|
||||
actor: actorFromSession(session),
|
||||
});
|
||||
revalidateTransferViews(transfer.subscriptionId);
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
return { ok: false, error: getErrorMessage(error, "拒收套餐 Push 失败") };
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelUserSubscriptionTransfer(transferId: string): Promise<UserTransferActionResult> {
|
||||
try {
|
||||
const session = await requireAuth();
|
||||
const id = transferIdSchema.parse(transferId);
|
||||
const transfer = await cancelSubscriptionTransfer({
|
||||
transferId: id,
|
||||
senderId: session.user.id,
|
||||
actor: actorFromSession(session),
|
||||
});
|
||||
revalidateTransferViews(transfer.subscriptionId);
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
return { ok: false, error: getErrorMessage(error, "取消套餐 Push 失败") };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user