refactor: 修复 modules 三层架构违规
- auth: actionDeleteAccount 改用 service+repo,forgot-password 完整三层实现 - card: serviceCheckCardOwnership 替代直接调用 repository - deck: 移除 service 层的 use server 指令 - dictionary: 数据转换逻辑从 repository 移到 service - ocr: 认证移到 action 层,跨模块调用改用 service - translator: genIPA/genLanguage 改用 service 层
This commit is contained in:
@@ -5,7 +5,6 @@ import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ValidateError } from "@/lib/errors";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
ActionInputGetUserProfileByUsername,
|
||||
ActionInputSignIn,
|
||||
@@ -20,7 +19,8 @@ import {
|
||||
import {
|
||||
serviceGetUserProfileByUsername,
|
||||
serviceSignIn,
|
||||
serviceSignUp
|
||||
serviceSignUp,
|
||||
serviceDeleteAccount
|
||||
} from "./auth-service";
|
||||
|
||||
// Re-export types for use in components
|
||||
@@ -194,75 +194,11 @@ export async function actionDeleteAccount(): Promise<ActionOutputDeleteAccount>
|
||||
return { success: false, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
const userId = session.user.id;
|
||||
const result = await serviceDeleteAccount({ userId: session.user.id });
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Delete in correct order to avoid foreign key constraints
|
||||
// 1. Revlogs (depend on cards)
|
||||
await tx.revlog.deleteMany({
|
||||
where: { card: { note: { userId } } }
|
||||
});
|
||||
|
||||
// 2. Cards (depend on notes and decks)
|
||||
await tx.card.deleteMany({
|
||||
where: { note: { userId } }
|
||||
});
|
||||
|
||||
// 3. Notes (depend on note types)
|
||||
await tx.note.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
// 4. Note types
|
||||
await tx.noteType.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
// 5. Deck favorites
|
||||
await tx.deckFavorite.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
// 6. Decks
|
||||
await tx.deck.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
// 7. Follows (both as follower and following)
|
||||
await tx.follow.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ followerId: userId },
|
||||
{ followingId: userId }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// 8. Dictionary lookups
|
||||
await tx.dictionaryLookUp.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
// 9. Translation history
|
||||
await tx.translationHistory.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
// 10. Sessions
|
||||
await tx.session.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
// 11. Accounts
|
||||
await tx.account.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
// 12. Finally, delete the user
|
||||
await tx.user.delete({
|
||||
where: { id: userId }
|
||||
});
|
||||
});
|
||||
if (!result.success) {
|
||||
return { success: false, message: "Failed to delete account" };
|
||||
}
|
||||
|
||||
return { success: true, message: "Account deleted successfully" };
|
||||
} catch (e) {
|
||||
|
||||
@@ -25,3 +25,12 @@ export type RepoInputFindUserById = {
|
||||
export type RepoInputFindUserByEmail = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
// Delete user cascade types
|
||||
export type RepoInputDeleteUserCascade = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type RepoOutputDeleteUserCascade = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import {
|
||||
RepoInputFindUserByEmail,
|
||||
RepoInputFindUserById,
|
||||
RepoInputFindUserByUsername,
|
||||
RepoOutputUserProfile
|
||||
RepoInputDeleteUserCascade,
|
||||
RepoOutputUserProfile,
|
||||
RepoOutputDeleteUserCascade
|
||||
} from "./auth-repository-dto";
|
||||
|
||||
const log = createLogger("auth-repository");
|
||||
|
||||
export async function repoFindUserByUsername(dto: RepoInputFindUserByUsername): Promise<RepoOutputUserProfile> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username: dto.username },
|
||||
@@ -62,3 +67,68 @@ export async function repoFindUserByEmail(dto: RepoInputFindUserByEmail): Promis
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function repoDeleteUserCascade(dto: RepoInputDeleteUserCascade): Promise<RepoOutputDeleteUserCascade> {
|
||||
const { userId } = dto;
|
||||
|
||||
log.info("Starting cascade delete for user", { userId });
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.revlog.deleteMany({
|
||||
where: { card: { note: { userId } } }
|
||||
});
|
||||
|
||||
await tx.card.deleteMany({
|
||||
where: { note: { userId } }
|
||||
});
|
||||
|
||||
await tx.note.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
await tx.noteType.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
await tx.deckFavorite.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
await tx.deck.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
await tx.follow.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ followerId: userId },
|
||||
{ followingId: userId }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await tx.dictionaryLookUp.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
await tx.translationHistory.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
await tx.session.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
await tx.account.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
await tx.user.delete({
|
||||
where: { id: userId }
|
||||
});
|
||||
});
|
||||
|
||||
log.info("Cascade delete completed for user", { userId });
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -38,3 +38,11 @@ export type ServiceOutputUserProfile = {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
} | null;
|
||||
|
||||
export type ServiceInputDeleteAccount = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type ServiceOutputDeleteAccount = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { auth } from "@/auth";
|
||||
import {
|
||||
repoFindUserByUsername,
|
||||
repoFindUserById
|
||||
repoFindUserById,
|
||||
repoDeleteUserCascade
|
||||
} from "./auth-repository";
|
||||
import {
|
||||
ServiceInputGetUserProfileByUsername,
|
||||
ServiceInputGetUserProfileById,
|
||||
ServiceInputSignIn,
|
||||
ServiceInputSignUp,
|
||||
ServiceInputDeleteAccount,
|
||||
ServiceOutputAuth,
|
||||
ServiceOutputUserProfile
|
||||
ServiceOutputUserProfile,
|
||||
ServiceOutputDeleteAccount
|
||||
} from "./auth-service-dto";
|
||||
|
||||
/**
|
||||
@@ -92,3 +95,7 @@ export async function serviceGetUserProfileByUsername(dto: ServiceInputGetUserPr
|
||||
export async function serviceGetUserProfileById(dto: ServiceInputGetUserProfileById): Promise<ServiceOutputUserProfile> {
|
||||
return await repoFindUserById(dto);
|
||||
}
|
||||
|
||||
export async function serviceDeleteAccount(dto: ServiceInputDeleteAccount): Promise<ServiceOutputDeleteAccount> {
|
||||
return await repoDeleteUserCascade({ userId: dto.userId });
|
||||
}
|
||||
|
||||
12
src/modules/auth/forgot-password-action-dto.ts
Normal file
12
src/modules/auth/forgot-password-action-dto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const schemaActionInputForgotPassword = z.object({
|
||||
email: z.string().email("请输入有效的邮箱地址"),
|
||||
});
|
||||
|
||||
export type ActionInputForgotPassword = z.infer<typeof schemaActionInputForgotPassword>;
|
||||
|
||||
export interface ActionOutputForgotPassword {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
35
src/modules/auth/forgot-password-action.ts
Normal file
35
src/modules/auth/forgot-password-action.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
"use server";
|
||||
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import { validate } from "@/utils/validate";
|
||||
import { ValidateError } from "@/lib/errors";
|
||||
import {
|
||||
schemaActionInputForgotPassword,
|
||||
type ActionInputForgotPassword,
|
||||
type ActionOutputForgotPassword,
|
||||
} from "./forgot-password-action-dto";
|
||||
import { serviceRequestPasswordReset } from "./forgot-password-service";
|
||||
|
||||
const log = createLogger("forgot-password-action");
|
||||
|
||||
export async function actionRequestPasswordReset(
|
||||
input: unknown
|
||||
): Promise<ActionOutputForgotPassword> {
|
||||
try {
|
||||
const dto = validate(input, schemaActionInputForgotPassword) as ActionInputForgotPassword;
|
||||
|
||||
return await serviceRequestPasswordReset({ email: dto.email });
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return {
|
||||
success: false,
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
log.error("Password reset request failed", { error: e });
|
||||
return {
|
||||
success: false,
|
||||
message: "发送重置邮件失败,请稍后重试",
|
||||
};
|
||||
}
|
||||
}
|
||||
7
src/modules/auth/forgot-password-repository-dto.ts
Normal file
7
src/modules/auth/forgot-password-repository-dto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type RepoInputFindUserByEmail = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type RepoOutputFindUserByEmail = {
|
||||
id: string;
|
||||
} | null;
|
||||
19
src/modules/auth/forgot-password-repository.ts
Normal file
19
src/modules/auth/forgot-password-repository.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import {
|
||||
RepoInputFindUserByEmail,
|
||||
RepoOutputFindUserByEmail
|
||||
} from "./forgot-password-repository-dto";
|
||||
|
||||
const log = createLogger("forgot-password-repository");
|
||||
|
||||
export async function repoFindUserByEmail(dto: RepoInputFindUserByEmail): Promise<RepoOutputFindUserByEmail> {
|
||||
log.debug("Finding user by email", { email: dto.email });
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: dto.email },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
8
src/modules/auth/forgot-password-service-dto.ts
Normal file
8
src/modules/auth/forgot-password-service-dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type ServiceInputRequestPasswordReset = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type ServiceOutputRequestPasswordReset = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
34
src/modules/auth/forgot-password-service.ts
Normal file
34
src/modules/auth/forgot-password-service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { auth } from "@/auth";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import { repoFindUserByEmail } from "./forgot-password-repository";
|
||||
import {
|
||||
ServiceInputRequestPasswordReset,
|
||||
ServiceOutputRequestPasswordReset
|
||||
} from "./forgot-password-service-dto";
|
||||
|
||||
const log = createLogger("forgot-password-service");
|
||||
|
||||
export async function serviceRequestPasswordReset(dto: ServiceInputRequestPasswordReset): Promise<ServiceOutputRequestPasswordReset> {
|
||||
log.info("Processing password reset request", { email: dto.email });
|
||||
|
||||
const user = await repoFindUserByEmail({ email: dto.email });
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
message: "该邮箱未注册",
|
||||
};
|
||||
}
|
||||
|
||||
await auth.api.requestPasswordReset({
|
||||
body: {
|
||||
email: dto.email,
|
||||
redirectTo: "/reset-password",
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "重置密码邮件已发送,请检查您的邮箱",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user