Feat: AuthRouter & CleanNeteaseCache API

前端增加限制访问的路由,后端调整鉴权接口,增加清除网易云缓存接口
This commit is contained in:
奇趣保罗 2022-05-06 23:10:16 +08:00
parent 3bb999aed5
commit 2f09fcabe4
15 changed files with 427 additions and 60 deletions

View File

@ -7,6 +7,15 @@ datasource db {
url = env("DB_URL") url = env("DB_URL")
} }
model User {
id String @id @default(cuid())
name String @unique
email String @unique
password String
created_at DateTime @default(now())
updated_at DateTime?
}
model ACGM { model ACGM {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
title String? title String?

View File

@ -24,6 +24,13 @@ const songs: Prisma.ACGMCreateInput[] = [
bangumi: "这个美术部有问题", bangumi: "这个美术部有问题",
music_id: 435288259 music_id: 435288259
}, },
{
title: "みずきのテーマ",
artist: "吟",
album: "この美術部には問題がある! オリジナルサウンドトラックCD vol.1",
bangumi: "这个美术部有问题",
music_id: 435288260
},
{ {
title: "伝える勇気があったなら", title: "伝える勇気があったなら",
artist: "吟", artist: "吟",
@ -85,6 +92,14 @@ async function main() {
}); });
} }
await prisma.user.create({
data: {
name: "Paul",
email: "dreamer_paul@126.com",
password: "123456",
}
});
console.log(`Seeding finished.`); console.log(`Seeding finished.`);
} }

View File

@ -0,0 +1,66 @@
// React
import React from "react";
import { useRequest } from "ahooks";
import useStat from "@/hooks/useStat";
// UI
import Button from "@/components/Base/Button";
// Tool
import addMusicRequest from "@/server/api/admin/acgm/add";
// Interface
import { MouseEvent, ChangeEvent } from "react";
// Components
function AddACGM() {
const { loading, run } = useRequest(
(params) => addMusicRequest({
query: params
}),
{
manual: true,
onSuccess: (data) => {
console.log(data);
}
}
);
const [input, setInput] = useStat({
id: "",
bangumi: ""
});
const onSubmit = (ev: MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
run(input);
}
const onInputChange = (ev: ChangeEvent<HTMLInputElement>) => {
setInput({ [ev.target.name]: ev.target.value });
}
return (
<fieldset>
<label>
<span></span>
<div className="input-group">
<input type="text" name="id" placeholder="输入网易云 ID"
value={input.id} onChange={onInputChange}
/>
<input type="text" name="bangumi" placeholder="输入番剧名称"
value={input.bangumi} onChange={onInputChange}
/>
<Button onClick={onSubmit} loading={loading}></Button>
</div>
</label>
</fieldset>
)
}
export default AddACGM;

View File

@ -0,0 +1,62 @@
// React
import React from "react";
import { useRequest } from "ahooks";
import useStat from "@/hooks/useStat";
// UI
import Button from "@/components/Base/Button";
// Tool
import cleanCacheRequest from "@/server/api/admin/netease/clean";
// Interface
import { MouseEvent, ChangeEvent } from "react";
// Components
function RemoveMusicCache() {
const { loading, run } = useRequest(
(params) => cleanCacheRequest({
query: params
}),
{
manual: true,
onSuccess: (data) => {
console.log(data);
}
}
);
const [input, setInput] = useStat({
id: ""
});
const onSubmit = (ev: MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
run(input);
}
const onInputChange = (ev: ChangeEvent<HTMLInputElement>) => {
setInput({ [ev.target.name]: ev.target.value });
}
return (
<fieldset>
<label>
<span></span>
<div className="input-group">
<input type="text" name="id" placeholder="输入网易云 ID"
value={input.id} onChange={onInputChange}
/>
<Button onClick={onSubmit} loading={loading}></Button>
</div>
</label>
</fieldset>
)
}
export default RemoveMusicCache;

View File

@ -0,0 +1,23 @@
// React
import React from "react";
// Interface
interface ButtonProps {
loading?: boolean
children: React.ReactNode
onClick?: React.MouseEventHandler<HTMLElement>
}
// Component
function Button({ loading = false, children, onClick }: ButtonProps) {
return (
<button className="btn green" onClick={onClick}>
{loading ? <i className="fa fa-spinner fa-spin"></i> : children}
</button>
)
}
export default Button;

View File

@ -47,6 +47,7 @@ function Aside() {
<Link to="/acgm"></Link> <Link to="/acgm"></Link>
<Link to="/bili"></Link> <Link to="/bili"></Link>
<Link to="/bing"></Link> <Link to="/bing"></Link>
{profile.name && <Link to="/admin"></Link>}
</nav> </nav>
</nav> </nav>
</nav> </nav>

View File

@ -1,11 +1,9 @@
// React // React
import React, { useEffect, useLayoutEffect } from "react"; import React, { useEffect, useLayoutEffect } from "react";
import useGlobalData from "@/hooks/useGlobalData";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
// Components // Components
import GlobalContext from "@/components/GlobalContext";
import Aside from "@/components/Layout/Aside"; import Aside from "@/components/Layout/Aside";
import Footer from "@/components/Layout/Footer"; import Footer from "@/components/Layout/Footer";
@ -20,7 +18,6 @@ interface FrontWrapperProps {
// Components // Components
function FrontWrapper(props: FrontWrapperProps) { function FrontWrapper(props: FrontWrapperProps) {
const location = useLocation(); const location = useLocation();
let [globalData, setGlobalData] = useGlobalData();
useEffect(() => { useEffect(() => {
const name = import.meta.env.PAUL_SITENAME; const name = import.meta.env.PAUL_SITENAME;
@ -38,11 +35,11 @@ function FrontWrapper(props: FrontWrapperProps) {
}, [location.pathname]); }, [location.pathname]);
return ( return (
<GlobalContext.Provider value={{ globalData, setGlobalData }}> <>
<Aside /> <Aside />
{props.element} {props.element}
<Footer /> <Footer />
</GlobalContext.Provider> </>
); );
} }

View File

@ -0,0 +1,63 @@
// React
import React, { useEffect, useLayoutEffect } from "react";
import { useLocation } from "react-router-dom";
// Components
import { Suspense } from "react";
import { Navigate } from "react-router-dom";
import Aside from "@/components/Layout/Aside";
import Footer from "@/components/Layout/Footer";
// Interface
import { ComponentType, LazyExoticComponent } from "react";
interface FrontWrapperProps {
title?: string
element: LazyExoticComponent<ComponentType<any>>
}
// Components
function FrontWrapperAuth(props: FrontWrapperProps) {
const location = useLocation();
const auth = document.cookie.includes("paul-logined");
useEffect(() => {
const name = import.meta.env.PAUL_SITENAME;
if (props.title) {
document.title = `${props.title} - ${name}`;
}
else if (name) {
document.title = String(name);
}
}, [location.pathname]);
useLayoutEffect(() => {
window.scrollTo({ top: 0, left: 0 });
}, [location.pathname]);
return (
auth ? (
<>
<Aside />
<Suspense fallback={<i className="fa fa-spinner fa-spin"></i>}>
<props.element />
</Suspense>
<Footer />
</>
) : (
<Navigate
to={{
search: "?go=" + location.pathname,
pathname: "/login"
}}
/>
)
);
}
export default FrontWrapperAuth;

View File

@ -52,6 +52,39 @@ blockquote.notice p{
100% { transform: translateX(-100%) } 100% { transform: translateX(-100%) }
} }
/* 输入框组 */
.input-group{ display: flex }
.input-group > input:first-child, .input-group > textarea:first-child, .input-group > select:first-child{
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > input, .input-group > textarea, .input-group > select{
flex: 1;
}
.input-group > input:not(:first-child), .input-group > textarea:not(:first-child), .input-group > select:not(:first-child){
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group > input:not(:last-child), .input-group > textarea:not(:last-child), .input-group > select:not(:last-child){
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > button:last-child{
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group > button .fa{
width: 1em;
text-align: center;
}
/* 按钮组 */ /* 按钮组 */
.btn-group{ .btn-group{
row-gap: .5em; row-gap: .5em;

View File

@ -1,13 +1,15 @@
import React from "react"; import React, { lazy } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import useGlobalData from "@/hooks/useGlobalData";
import GlobalContext from "@/components/GlobalContext";
import "./kico.css"; import "./kico.css";
import "./index.css"; import "./index.css";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
// Routes // Routes
import Home from "./pages/index"; import Home from "./pages/index";
import Login from "./pages/login"; import Login from "./pages/login";
@ -23,45 +25,54 @@ import NoMatch from "./pages/404";
// RouteWrapper // RouteWrapper
import FrontWrapper from "./components/Layout/FrontWrapper"; import FrontWrapper from "./components/Layout/FrontWrapper";
import FrontWrapperAuth from "./components/Layout/FrontWrapperAuth";
const admin = lazy(() => import("./pages/admin"));
// Components // Components
function App() { function App() {
let [globalData, setGlobalData] = useGlobalData();
return ( return (
<Router> <GlobalContext.Provider value={{ globalData, setGlobalData }}>
<Routes> <Router>
<Route path="/" element={ <Routes>
<FrontWrapper element={<Home />} />} <Route path="/" element={
/> <FrontWrapper element={<Home />} />}
<Route path="/login" element={ />
<FrontWrapper title="登录" element={<Login />} /> } <Route path="/login" element={
/> <FrontWrapper title="登录" element={<Login />} /> }
<Route path="/notice" element={ />
<FrontWrapper title="使用约定" element={<Notice />} /> } <Route path="/admin" element={
/> <FrontWrapperAuth title="轻管理" element={admin} /> }
<Route path="/log" element={ />
<FrontWrapper title="更新记录" element={<Log />} />} <Route path="/notice" element={
/> <FrontWrapper title="使用约定" element={<Notice />} /> }
<Route path="/netease" element={ />
<FrontWrapper title="网易云解析" element={<Netease />} />} <Route path="/log" element={
/> <FrontWrapper title="更新记录" element={<Log />} />}
<Route path="/wallpaper" element={ />
<FrontWrapper title="随机动漫壁纸" element={<Wallpaper />} />} <Route path="/netease" element={
/> <FrontWrapper title="网易云解析" element={<Netease />} />}
<Route path="/acgm" element={ />
<FrontWrapper title="随机动漫音乐" element={<ACGM />} />} <Route path="/wallpaper" element={
/> <FrontWrapper title="随机动漫壁纸" element={<Wallpaper />} />}
<Route path="/bili" element={ />
<FrontWrapper title="哔哩哔哩小窗" element={<Bili />} />} <Route path="/acgm" element={
/> <FrontWrapper title="随机动漫音乐" element={<ACGM />} />}
<Route path="/bing" element={ />
<FrontWrapper title="必应每日壁纸" element={<Bing />} />} <Route path="/bili" element={
/> <FrontWrapper title="哔哩哔哩小窗" element={<Bili />} />}
<Route path="*" element={ />
<FrontWrapper title="404" element={<NoMatch />} />} <Route path="/bing" element={
/> <FrontWrapper title="必应每日壁纸" element={<Bing />} />}
</Routes> />
</Router> <Route path="*" element={
<FrontWrapper title="404" element={<NoMatch />} />}
/>
</Routes>
</Router>
</GlobalContext.Provider>
); );
} }

27
src/pages/admin.tsx Normal file
View File

@ -0,0 +1,27 @@
// React
import React from "react";
// UI
import Button from "@/components/Base/Button";
import ArticleHead from "@/components/Layout/ArticleHead";
import AddACGM from "@/components/Admin/AddACGM";
import RemoveMusicCache from "@/components/Admin/RemoveMusicCache";
// Components
function Admin() {
return (
<main>
<ArticleHead title="轻管理" desc="简单的维护功能" />
<form>
<AddACGM />
<RemoveMusicCache />
</form>
</main>
)
}
export default Admin;

View File

@ -31,12 +31,17 @@ function Login() {
onSuccess: (data) => { onSuccess: (data) => {
console.log(data); console.log(data);
setGlobalData({ if (data.data) {
type: "profile", setGlobalData({
value: data.profile type: "profile",
}); value: data.data.profile
});
navigate("/") navigate("/");
}
else {
alert("错误的用户名或密码");
}
} }
} }
); );

View File

@ -0,0 +1,29 @@
import { Api, Post, Query, useContext, useInject, Middleware } from "@midwayjs/hooks";
import { RedisService } from "@midwayjs/redis";
import { JwtPassportMiddleware } from "../../../middleware/jwt.middleware";
export default Api(
Post(),
Middleware(JwtPassportMiddleware),
Query<{ id: string, bangumi: string }>(),
async () => {
const ctx = useContext();
const client = await useInject(RedisService);
const { id } = ctx.query;
if (!id) {
return {
code: 0,
msg: "Required id"
}
}
await client.del(`api-next:163:${id}`);
return {
code: 1,
msg: "Success"
};
}
);

View File

@ -1,5 +1,6 @@
import { Api, Get, Query, useContext, useInject } from "@midwayjs/hooks"; import { Api, Get, Query, useContext, useInject } from "@midwayjs/hooks";
import { JwtService } from '@midwayjs/jwt'; import { JwtService } from "@midwayjs/jwt";
import { prisma } from "../../utils/prisma";
export default Api( export default Api(
Get(), Get(),
@ -11,17 +12,42 @@ export default Api(
const token = await jwt.sign({ name: "Paul" }); const token = await jwt.sign({ name: "Paul" });
const user = await prisma.user.findFirst({
where: {
name: ctx.query.username,
password: ctx.query.password
}
});
if (!user) {
return {
code: 0,
msg: "Invalid username or password"
}
}
// Token
ctx.cookies.set("paul-token", token, { ctx.cookies.set("paul-token", token, {
maxAge: 60 * 60 * 24 * 2, maxAge: 60 * 60 * 24 * 2 * 100,
httpOnly: true httpOnly: true
}); });
// 标记已登录
ctx.cookies.set("paul-logined", 1, {
maxAge: 60 * 60 * 24 * 2 * 100,
httpOnly: false
});
return { return {
profile: { code: 1,
name: "Paul", msg: "Success",
avatar: "https://sdn.geekzu.org/avatar/d22eb460ecab37fcd7205e6a3c55c228?s=200&r=X&d=", data: {
}, profile: {
token name: "Paul",
avatar: "https://sdn.geekzu.org/avatar/d22eb460ecab37fcd7205e6a3c55c228?s=200&r=X&d=",
},
token
}
} }
} }
); );

View File

@ -1,18 +1,18 @@
import { useContext } from "@midwayjs/hooks"; import { useContext } from "@midwayjs/hooks";
import { CustomStrategy, PassportStrategy } from '@midwayjs/passport'; import { CustomStrategy, PassportStrategy } from "@midwayjs/passport";
import { Strategy, ExtractJwt } from 'passport-jwt'; import { Strategy, ExtractJwt } from "passport-jwt";
import { Config } from '@midwayjs/decorator'; import { Config } from "@midwayjs/decorator";
@CustomStrategy() @CustomStrategy()
export class JwtStrategy extends PassportStrategy( export class JwtStrategy extends PassportStrategy(
Strategy, Strategy,
'jwt' "jwt"
) { ) {
@Config('jwt') @Config("jwt")
jwtConfig; jwtConfig: any;
async validate(payload) { async validate(payload) {
console.log('payload', payload); console.log("payload", payload);
return payload; return payload;
} }