Skip to content

Commit

Permalink
Merge pull request #143 from serafuku/yunochi/develop
Browse files Browse the repository at this point in the history
✨ 계정 삭제기능 추가 + 버그수정
  • Loading branch information
Squarecat-meow authored Jan 13, 2025
2 parents fdff711 + b229c27 commit 779817e
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 50 deletions.
6 changes: 6 additions & 0 deletions src/app/_dto/account-delete/account-delete.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsString } from "class-validator";

export class AccountDeleteReqDto {
@IsString()
handle: string;
}
50 changes: 50 additions & 0 deletions src/app/api/_service/account/account-delete.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { AccountDeleteReqDto } from '@/app/_dto/account-delete/account-delete.dto';
import { RateLimit } from '@/app/api/_service/ratelimiter/decorator';
import { sendApiError } from '@/app/api/_utils/apiErrorResponse/sendApiError';
import { GetPrismaClient } from '@/app/api/_utils/getPrismaClient/get-prisma-client';
import { Auth, JwtPayload } from '@/app/api/_utils/jwt/decorator';
import { jwtPayloadType } from '@/app/api/_utils/jwt/jwtPayloadType';
import { Body, ValidateBody } from '@/app/api/_utils/Validator/decorator';
import { Logger } from '@/utils/logger/Logger';
import { PrismaClient } from '@prisma/client';
import { NextRequest, NextResponse } from 'next/server';

export class AccountDeleteService {
private logger: Logger;
private prisma: PrismaClient;
private static instance: AccountDeleteService;
private constructor() {
this.logger = new Logger('AccountDeleteService');
this.prisma = GetPrismaClient.getClient();
}
public static getInstance() {
if (!AccountDeleteService.instance) {
AccountDeleteService.instance = new AccountDeleteService();
}
return AccountDeleteService.instance;
}

@ValidateBody(AccountDeleteReqDto)
@Auth()
@RateLimit({ bucket_time: 60, req_limit: 60 }, 'ip')
public async deleteAccountApi(
_req: NextRequest,
@Body body: AccountDeleteReqDto,
@JwtPayload tokenBody: jwtPayloadType,
): Promise<NextResponse> {
if (body.handle !== tokenBody.handle) {
return sendApiError(401, 'Handle not match with JWT handle', 'UNAUTHORIZED');
}
const user = await this.prisma.user.findUniqueOrThrow({ where: { handle: tokenBody.handle } });
this.logger.log(`Delete user ${user.handle}`);
await this.prisma.question.deleteMany({ where: { questioneeHandle: user.handle } });
await this.prisma.answer.deleteMany({ where: { answeredPersonHandle: user.handle } });
await this.prisma.following.deleteMany({ where: { followerHandle: user.handle } });
await this.prisma.blocking.deleteMany({ where: { blockerHandle: user.handle } });
await this.prisma.notification.deleteMany({ where: { userHandle: user.handle } });
await this.prisma.profile.delete({ where: { handle: user.handle } });
await this.prisma.user.delete({ where: { handle: user.handle } });
this.logger.log(`User ${user.handle} deleted!`);
return NextResponse.json({ message: 'Good bye' }, { status: 200 });
}
}
21 changes: 15 additions & 6 deletions src/app/api/_service/answer/answer-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,15 +354,27 @@ export class AnswerService {
}

@RateLimit({ bucket_time: 300, req_limit: 600 }, 'ip')
public async GetSingleAnswerApi(_req: NextRequest, answerId: string) {
public async GetSingleAnswerApi(_req: NextRequest, answerId: string, userHandle: string) {
const dto = await this.GetSingleAnswerDto(answerId, userHandle);
if (!dto) {
return sendApiError(404, 'Not found', 'NOT_FOUND');
}
return NextResponse.json(dto, {
status: 200,
headers: { 'Content-type': 'application/json', 'Cache-Control': 'public, max-age=60' },
});
}

public async GetSingleAnswerDto(answerId: string, userHandle: string): Promise<AnswerWithProfileDto | undefined> {
const answer = await this.prisma.answer.findUnique({
include: { answeredPerson: { include: { user: { include: { server: { select: { instanceType: true } } } } } } },
where: {
id: answerId,
answeredPersonHandle: userHandle,
},
});
if (!answer) {
return sendApiError(404, 'Not found', 'NOT_FOUND');
return;
}
const profileDto = profileToDto(
answer.answeredPerson,
Expand All @@ -374,10 +386,7 @@ export class AnswerService {
...answerDto,
answeredPerson: profileDto,
};
return NextResponse.json(dto, {
status: 200,
headers: { 'Content-type': 'application/json', 'Cache-Control': 'public, max-age=60' },
});
return dto;
}

public async filterBlock(answers: AnswerWithProfileDto[], myHandle: string) {
Expand Down
6 changes: 3 additions & 3 deletions src/app/api/db/answers/[userHandle]/[answerId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ a
return await service.deleteAnswer(req, answerId, null as unknown as jwtPayloadType);
}

export async function GET(req: NextRequest, { params }: { params: Promise<{ answerId: string }> }) {
export async function GET(req: NextRequest, { params }: { params: Promise<{ userHandle:string, answerId: string }> }) {
const service = AnswerService.getInstance();
const { answerId } = await params;
return await service.GetSingleAnswerApi(req, answerId);
const { answerId, userHandle } = await params;
return await service.GetSingleAnswerApi(req, answerId, userHandle);
}
8 changes: 8 additions & 0 deletions src/app/api/user/account-delete/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AccountDeleteService } from "@/app/api/_service/account/account-delete.service";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest): Promise<NextResponse> {
const deleteService = AccountDeleteService.getInstance();
return await deleteService.deleteAccountApi(req, null as any, null as any);
}
80 changes: 68 additions & 12 deletions src/app/main/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import CollapseMenu from '@/app/_components/collapseMenu';
import DialogModalTwoButton from '@/app/_components/modalTwoButton';
import { AccountCleanReqDto } from '@/app/_dto/account-clean/account-clean.dto';
import { FaLock, FaUserLargeSlash } from 'react-icons/fa6';
import { MdDeleteSweep, MdOutlineCleaningServices } from 'react-icons/md';
import { MdDeleteForever, MdDeleteSweep, MdOutlineCleaningServices } from 'react-icons/md';
import { MyProfileContext } from '@/app/main/layout';
import { MyProfileEv } from '@/app/main/_events';
import { getProxyUrl } from '@/utils/getProxyUrl/getProxyUrl';
import { onApiError } from '@/utils/api-error/onApiError';
import { AccountDeleteReqDto } from '@/app/_dto/account-delete/account-delete.dto';

export type FormValue = {
stopAnonQuestion: boolean;
Expand Down Expand Up @@ -68,6 +69,7 @@ export default function Settings() {
const [defaultFormValue, setDefaultFormValue] = useState<FormValue>();
const logoutAllModalRef = useRef<HTMLDialogElement>(null);
const accountCleanModalRef = useRef<HTMLDialogElement>(null);
const accountDeleteModalRef = useRef<HTMLDialogElement>(null);
const importBlockModalRef = useRef<HTMLDialogElement>(null);
const deleteAllQuestionsModalRef = useRef<HTMLDialogElement>(null);
const deleteAllNotificationsModalRef = useRef<HTMLDialogElement>(null);
Expand Down Expand Up @@ -146,6 +148,31 @@ export default function Settings() {
}, 2000);
};

const onAccountDelete = async () => {
setButtonClicked(true);
setTimeout(() => {
setButtonClicked(false);
}, 2000);
const user_handle = userInfo?.handle;
if (!user_handle) {
return;
}
const req: AccountDeleteReqDto = {
handle: user_handle,
};
const res = await fetch('/api/user/account-delete', {
method: 'POST',
body: JSON.stringify(req),
});
if (res.ok) {
localStorage.removeItem('user_handle');
localStorage.removeItem('last_token_refresh');
window.location.replace('/');
} else {
onApiError(res.status, res);
}
};

const onImportBlock = async () => {
setButtonClicked(true);
const res = await fetch('/api/user/blocking/import', {
Expand Down Expand Up @@ -327,6 +354,23 @@ export default function Settings() {
{buttonClicked ? '잠깐만요...' : '알림함 비우기'}
</button>
<Divider />
<div className="font-normal text-xl py-3 flex items-center gap-2">
<MdDeleteSweep size={24} />
미답변 질문 비우기
</div>
<div className="font-thin px-4 py-2 break-keep">
아직 답변하지 않은 모든 질문들을 지워요. 지워진 질문은 되돌릴 수 없으니 주의하세요.
</div>
<button
type="button"
onClick={() => {
deleteAllQuestionsModalRef.current?.showModal();
}}
className={`btn ${buttonClicked ? 'btn-disabled' : 'btn-warning'}`}
>
{buttonClicked ? '잠깐만요...' : '모든 질문을 삭제'}
</button>
<Divider />
<div className="font-normal text-xl py-3 flex items-center gap-2">
<FaUserLargeSlash />
차단 목록 가져오기
Expand Down Expand Up @@ -362,22 +406,24 @@ export default function Settings() {
>
{buttonClicked ? '잠깐만요...' : '모든 답변을 삭제'}
</button>

<Divider />
<div className="font-normal text-xl py-3 flex items-center gap-2">
<MdDeleteSweep size={24} />
모든 질문 삭제하기
<MdDeleteForever size={24} />
계정 삭제하기
</div>
<div className="font-thin px-4 py-2 break-keep">
아직 답변하지 않은 모든 질문들을 지워요. 지워진 글은 되돌릴 수 없으니 주의하세요.
네오 퀘스돈에서 이 계정을 삭제해요. 이 계정으로 했던 모든 활동은 지워져요. 이 작업은 되돌릴
수 없으니 주의하세요.
</div>
<button
type="button"
onClick={() => {
deleteAllQuestionsModalRef.current?.showModal();
accountDeleteModalRef.current?.showModal();
}}
className={`btn ${buttonClicked ? 'btn-disabled' : 'btn-error'}`}
>
{buttonClicked ? '잠깐만요...' : '모든 질문을 삭제'}
{buttonClicked ? '잠깐만요...' : '계정 삭제'}
</button>
</div>
</CollapseMenu>
Expand All @@ -404,13 +450,23 @@ export default function Settings() {
onClick={onDeleteAllNotifications}
/>
<DialogModalTwoButton
title={'경고'}
body={'미답변 질문들을 모두 지울까요? \n이 작업은 시간이 걸리고, 지워진 질문은 복구할 수 없어요!'}
title={'주의'}
body={
'아직 답변하지 않은 질문들을 모두 지울까요? \n이 작업은 시간이 걸리고, 지워진 질문은 복구할 수 없어요!'
}
confirmButtonText={'네'}
cancelButtonText={'아니오'}
ref={deleteAllQuestionsModalRef}
onClick={onDeleteAllQuestions}
/>
<DialogModalTwoButton
title={'주의'}
body={`${userInfo.instanceType} 에서 차단 목록을 가져올까요? \n 이 작업은 완료되는데 시간이 조금 걸려요!`}
confirmButtonText={'네'}
cancelButtonText={'아니오'}
ref={importBlockModalRef}
onClick={onImportBlock}
/>
<DialogModalTwoButton
title={'경고'}
body={'그동안 썼던 모든 답변을 지울까요? \n이 작업은 시간이 걸리고, 지워진 답변은 복구할 수 없어요!'}
Expand All @@ -420,12 +476,12 @@ export default function Settings() {
onClick={onAccountClean}
/>
<DialogModalTwoButton
title={'주의'}
body={`${userInfo.instanceType} 에서 블락 목록을 가져올까요? \n 이 작업은 완료되는데 시간이 조금 걸려요!`}
title={'경고'}
body={'정말 계정을 삭제할까요...? \n 이 계정의 모든 정보가 지워져요!'}
confirmButtonText={'네'}
cancelButtonText={'아니오'}
ref={importBlockModalRef}
onClick={onImportBlock}
ref={accountDeleteModalRef}
onClick={onAccountDelete}
/>
</>
)}
Expand Down
10 changes: 10 additions & 0 deletions src/app/main/user/[handle]/[answer]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function AnswerNotFoundPage() {
return (
<div className="w-[90%] window:w-[80%] desktop:w-[70%] grid grid-cols-1 desktop:grid-cols-2 gap-4">
<div className="w-full col-span-2 flex flex-col justify-center items-center glass text-4xl rounded-box shadow p-2">
😶‍🌫️
<span> 답변을 찾을 수 없어요! </span>
</div>
</div>
);
}
13 changes: 5 additions & 8 deletions src/app/main/user/[handle]/[answer]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Metadata } from 'next';
import SingleAnswer from './answer';
import { AnswerWithProfileDto } from '@/app/_dto/answers/Answers.dto';
import { notFound } from 'next/navigation';
import { AnswerService } from '@/app/api/_service/answer/answer-service';

export const dynamic = 'force-dynamic';

Expand Down Expand Up @@ -29,16 +30,12 @@ export async function generateMetadata({
}

async function fetchAnswer(userHandle: string, id: string): Promise<AnswerWithProfileDto | undefined> {
const url = process.env.WEB_URL;
const res = await fetch(`${url}/api/db/answers/${userHandle}/${id}`, {
method: 'GET',
});
if (res.status === 404) {
const answerService = AnswerService.getInstance();
const answer = await answerService.GetSingleAnswerDto(id, userHandle);
if (!answer) {
return undefined;
} else if (!res.ok) {
throw new Error(`Fail to fetch answer! ${await res.text()}`);
}
return await res.json();
return answer;
}

export default async function singleAnswerWrapper({ params }: { params: Promise<{ handle: string; answer: string }> }) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/main/user/[handle]/_answers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function fetchProfile(handle: string) {
onApiError(profile.status, profile);
return undefined;
}
} catch (err) {
} catch {
return undefined;
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/app/main/user/[handle]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function UserNotFoundPage() {
return (
<div className="w-[90%] window:w-[80%] desktop:w-[70%] grid grid-cols-1 desktop:grid-cols-2 gap-4">
<div className="w-full col-span-2 flex flex-col justify-center items-center glass text-4xl rounded-box shadow p-2">
😶‍🌫️
<span>그런 사용자는 없어요!</span>
</div>
</div>
);
}
29 changes: 9 additions & 20 deletions src/app/main/user/[handle]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Profile from '@/app/main/user/[handle]/_profile';
import josa from '@/app/api/_utils/josa';
import { Metadata } from 'next';
import { GetPrismaClient } from '@/app/api/_utils/getPrismaClient/get-prisma-client';
import { notFound } from 'next/navigation';

export const dynamic = 'force-dynamic';

Expand Down Expand Up @@ -42,28 +43,16 @@ export default async function ProfilePage({ params }: { params: Promise<{ handle
},
});

if (user === null) {
return notFound();
}
return (
<div className="w-[90%] window:w-[80%] desktop:w-[70%] grid grid-cols-1 desktop:grid-cols-2 gap-4">
{user === null ? (
<div className="w-full col-span-2 flex flex-col justify-center items-center glass text-4xl rounded-box shadow p-2">
😶‍🌫️
<span>그런 사용자는 없어요!</span>
</div>
) : (
<>
{user === undefined ? (
<div className="w-full col-span-2 flex justify-center">
<span className="loading loading-spinner loading-lg" />
</div>
) : (
<>
<a href={`https://${user.hostName}/@${user.account}`} className="hidden" rel={'me'}></a>
<Profile />
<UserPage />
</>
)}
</>
)}
<>
<a href={`https://${user.hostName}/@${user.account}`} className="hidden" rel={'me'}></a>
<Profile />
<UserPage />
</>
</div>
);
}

0 comments on commit 779817e

Please sign in to comment.