Skip to content

Commit

Permalink
feat: Add change password dialog & style: Unified the code style of h…
Browse files Browse the repository at this point in the history
…ttp request sending, processing and error handling
  • Loading branch information
NriotHrreion committed Aug 7, 2024
1 parent 933311b commit a86c936
Show file tree
Hide file tree
Showing 11 changed files with 233 additions and 61 deletions.
42 changes: 29 additions & 13 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useEffect } from "react";
import axios from "axios";
import axios, { type AxiosError } from "axios";
import md5 from "md5";
import Cookie from "js-cookie";
import { Button } from "@nextui-org/button";
Expand All @@ -19,7 +19,7 @@ import { BaseResponseData } from "@/types";
import { tokenStorageKey } from "@/lib/global";

const schema = z.object({
password: z.string().min(1, { message: "请输入访问密码" }),
password: z.string().min(6, { message: "请输入访问密码" }),
});

interface AuthResponseData extends BaseResponseData {
Expand All @@ -40,19 +40,35 @@ export default function Page() {
const { data } = await axios.post<AuthResponseData>("/api/auth", { password: md5(password) });
const { status, token } = data;

if(status === 401) throw new Error("访问密码错误");
if(status === 403) {
router.push("/");
throw new Error("已登录,无法重复登录");
if(!token) {
toast.error("服务器未返回token");

return;
}

if(status === 200) {
Cookie.set(tokenStorageKey, token);
toast.success("登录成功");
router.push("/explorer");
}
if(status === 500) throw new Error("服务器错误");
if(!token) throw new Error("服务器未返回token");
} catch (_err) {
const err = _err as AxiosError;
const status = err.response?.status ?? 0;

Cookie.set(tokenStorageKey, token);
toast.success("登录成功");
router.push("/explorer");
} catch (err) {
toast.error("登录失败: "+ err);
switch(status) {
case 400:
toast.error(`请求无效 (${status})`);
break;
case 401:
toast.error(`访问密码错误 (${status})`);
break;
case 403:
toast.error(`已登录,无法重复登录 (${status})`);
break;
case 500:
toast.error(`服务器内部错误 (${status})`);
break;
}
}
};

Expand Down
4 changes: 3 additions & 1 deletion app/(pages)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import ThemeSwitch from "@/components/theme-switch";
import { tokenStorageKey } from "@/lib/global";
import { scrollbarStyle } from "@/lib/style";
import { useWithSettings } from "@/hooks/useWithSettings";
import { useDialog } from "@/hooks/useDialog";

export default function Page() {
const router = useRouter();
const dialog = useDialog();
const { settings, set } = useWithSettings();

const handleLogout = () => {
Expand Down Expand Up @@ -108,7 +110,7 @@ export default function Page() {

<SettingsSection title="安全">
<SettingsItem label="设置访问密码">
<Button color="primary">开始设置</Button>
<Button color="primary" onPress={() => dialog.open("changePassword")}>开始设置</Button>
</SettingsItem>

<SettingsItem label="登出 Ferrum" description="退出Ferrum 并清除token信息">
Expand Down
42 changes: 38 additions & 4 deletions app/api/auth/route.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
/* eslint-disable no-console */
import fs from "node:fs";
import path from "node:path";

import { NextRequest } from "next/server";
import md5 from "md5";

import { generateToken } from "@/lib/token";
import { generateToken, validateToken } from "@/lib/token";
import { tokenStorageKey } from "@/lib/global";
import { packet, error } from "@/lib/packet";

interface AuthRequestData {
interface AuthPostRequestData {
password: string
}

export async function POST(req: NextRequest) {
try {
if(req.cookies.get(tokenStorageKey)) return error(403);

const { password } = await req.json() as AuthRequestData;
const { password } = await req.json() as AuthPostRequestData;

if(!password) return error(400);
const md5Password = md5(password);
Expand All @@ -24,7 +27,38 @@ export async function POST(req: NextRequest) {

return packet({ token });
} catch (err) {
console.log("[Server: /api/auth/login] "+ err);
console.log("[Server: /api/auth] "+ err);

return error(500);
}
}

interface AuthPatchRequestData {
newPassword: string
oldPassword: string
}

export async function PATCH(req: NextRequest) {
const token = req.cookies.get(tokenStorageKey)?.value;

if(!token) return error(401);
if(!validateToken(token)) return error(403);

try {
const { oldPassword, newPassword } = await req.json() as AuthPatchRequestData;

if(!oldPassword) return error(400);
const md5OldPassword = md5(oldPassword);

if(md5OldPassword !== process.env["PASSWORD"]) return error(401);

const md5NewPassword = md5(newPassword);

fs.writeFileSync(path.join(process.cwd(), ".env"), `PASSWORD=${md5NewPassword}`, "utf-8");

return packet({});
} catch (err) {
console.log("[Server: /api/auth] "+ err);

return error(500);
}
Expand Down
12 changes: 10 additions & 2 deletions app/api/fs/file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export async function GET(req: NextRequest) {
}
}

interface FilePostRequestData {
newName: string
}

export async function POST(req: NextRequest) {
const token = req.cookies.get(tokenStorageKey)?.value;

Expand All @@ -62,7 +66,7 @@ export async function POST(req: NextRequest) {

const { searchParams } = new URL(req.url);
const targetPath = searchParams.get("path") ?? "/";
const newName = (await req.formData()).get("newName");
const { newName } = await req.json() as FilePostRequestData;

if(!newName || /[\\\/:*?"<>|]/.test(newName.toString())) error(400);
const newPath = path.join(path.dirname(targetPath), newName?.toString() ?? "");
Expand All @@ -86,6 +90,10 @@ export async function POST(req: NextRequest) {
}
}

interface FilePatchRequestData {
content: string
}

export async function PATCH(req: NextRequest) {
const token = req.cookies.get(tokenStorageKey)?.value;

Expand All @@ -94,7 +102,7 @@ export async function PATCH(req: NextRequest) {

const { searchParams } = new URL(req.url);
const targetPath = searchParams.get("path") ?? "/";
const content = (await req.formData()).get("content");
const { content } = await req.json() as FilePatchRequestData;

if(!content) error(400);

Expand Down
15 changes: 11 additions & 4 deletions app/api/fs/folder/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export async function GET(req: NextRequest) {
}
}

interface FolderPostRequestData {
newName: string
}

export async function POST(req: NextRequest) {
const token = req.cookies.get(tokenStorageKey)?.value;

Expand All @@ -66,7 +70,7 @@ export async function POST(req: NextRequest) {
const { searchParams } = new URL(req.url);
const targetDisk = searchParams.get("disk");
const targetPath = path.join(targetDisk ?? "C:", searchParams.get("path") ?? "/");
const newName = (await req.formData()).get("newName");
const { newName } = await req.json() as FolderPostRequestData;

if(!newName || /[\\\/:*?"<>|]/.test(newName.toString())) error(400);
const newPath = path.join(path.dirname(targetPath), newName?.toString() ?? "");
Expand Down Expand Up @@ -118,6 +122,11 @@ export async function DELETE(req: NextRequest) {
}
}

interface FolderPutRequestData {
name: string
type: "folder" | "file"
}

export async function PUT(req: NextRequest) {
const token = req.cookies.get(tokenStorageKey)?.value;

Expand All @@ -128,9 +137,7 @@ export async function PUT(req: NextRequest) {
const targetDisk = searchParams.get("disk");
const targetPath = path.join(targetDisk ?? "C:", searchParams.get("path") ?? "/");

const formData = await req.formData();
const name = formData.get("name");
const type = formData.get("type");
const { name, type } = await req.json() as FolderPutRequestData;

if(!name || !type || /[\\\/:*?"<>|]/.test(name.toString())) error(400);
const newPath = path.join(targetPath, name?.toString() ?? "");
Expand Down
2 changes: 2 additions & 0 deletions app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import RemoveFileDialog from "@/components/dialogs/remove-file-dialog";
import CreateFolderDialog from "@/components/dialogs/create-folder-dialog";
import CreateFileDialog from "@/components/dialogs/create-file-dialog";
import UploadFileDialog from "@/components/dialogs/upload-file-dialog";
import ChangePasswordDialog from "@/components/dialogs/change-password-dialog";

export interface ProvidersProps {
children: React.ReactNode;
Expand All @@ -40,6 +41,7 @@ export function Providers({ children, themeProps }: ProvidersProps) {
<CreateFolderDialog />
<CreateFileDialog />
<UploadFileDialog />
<ChangePasswordDialog />
</NextThemesProvider>
</NextUIProvider>
);
Expand Down
135 changes: 135 additions & 0 deletions components/dialogs/change-password-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"use-client";

import React from "react";
import { useRouter } from "next/navigation";
import md5 from "md5";
import axios, { type AxiosError } from "axios";
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@nextui-org/modal";
import { Button } from "@nextui-org/button";
import { Input } from "@nextui-org/input";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { toast } from "react-toastify";
import Cookies from "js-cookie";

import { DialogStore, useDialog } from "@/hooks/useDialog";
import { tokenStorageKey } from "@/lib/global";

const schema = z.object({
oldPassword: z.string().min(6, { message: "请输入当前密码" }),
newPassword: z.string().min(6, { message: "请输入新密码,且密码长度应至少6位" }),
});

const ChangePasswordDialog: React.FC = () => {
const { type, isOpened, close } = useDialog() as DialogStore<{}>;
const router = useRouter();

const isCurrentDialog = isOpened && type === "changePassword";

const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
oldPassword: "",
newPassword: ""
}
});

const handleEnter = async ({ oldPassword, newPassword }: z.infer<typeof schema>) => {
if(oldPassword === newPassword) {
toast.warn("新密码不可与当前密码一致");

return;
}

if(newPassword.length < 6) {
toast.warn("密码长度应至少为6位");

return;
}

try {
const { status } = await axios.patch("/api/auth", {
oldPassword: md5(oldPassword),
newPassword: md5(newPassword)
});

if(status === 200) {
toast.success("密码修改成功");
handleClose();

// Logout
Cookies.remove(tokenStorageKey);
router.refresh();
}
} catch (_err) {
const err = _err as AxiosError;
const status = err.response?.status ?? 0;

switch(status) {
case 400:
toast.error(`请求无效 (${status})`);
break;
case 401:
toast.error(`访问密码错误 (${status})`);
break;
case 403:
toast.error(`无效的访问token (${status})`);
break;
case 500:
toast.error(`服务器内部错误 (${status})`);
break;
}
}
};

const handleClose = () => {
form.reset();
close();
};

if(!isCurrentDialog) return <></>;

return (
<Modal isOpen={isCurrentDialog} onOpenChange={(isOpened) => (!isOpened && handleClose())}>
<ModalContent>
<form onSubmit={form.handleSubmit(handleEnter)}>
<ModalHeader>设置访问密码</ModalHeader>
<ModalBody>
<Input
{...form.register("oldPassword")}
isRequired
isInvalid={form.formState.errors.oldPassword !== undefined}
label="当前密码"
type="password"
labelPlacement="outside"
autoComplete="off"
errorMessage={form.formState.errors.oldPassword?.message}/>
<Input
{...form.register("newPassword")}
isRequired
isInvalid={form.formState.errors.newPassword !== undefined}
label="新密码"
type="password"
labelPlacement="outside"
autoComplete="off"
errorMessage={form.formState.errors.newPassword?.message}/>
</ModalBody>
<ModalFooter>
<Button
size="sm"
color="danger"
variant="light"
onPress={() => handleClose()}>取消</Button>
<Button
size="sm"
color="primary"
type="submit">确定</Button>
</ModalFooter>
</form>
</ModalContent>
</Modal>
);
}

export default ChangePasswordDialog;
Loading

0 comments on commit a86c936

Please sign in to comment.