Feat: AuthRouter & CleanNeteaseCache API
前端增加限制访问的路由,后端调整鉴权接口,增加清除网易云缓存接口
This commit is contained in:
parent
3bb999aed5
commit
2f09fcabe4
|
|
@ -7,6 +7,15 @@ datasource db {
|
|||
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 {
|
||||
id Int @id @default(autoincrement())
|
||||
title String?
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ const songs: Prisma.ACGMCreateInput[] = [
|
|||
bangumi: "这个美术部有问题",
|
||||
music_id: 435288259
|
||||
},
|
||||
{
|
||||
title: "みずきのテーマ",
|
||||
artist: "吟",
|
||||
album: "この美術部には問題がある! オリジナルサウンドトラックCD vol.1",
|
||||
bangumi: "这个美术部有问题",
|
||||
music_id: 435288260
|
||||
},
|
||||
{
|
||||
title: "伝える勇気があったなら",
|
||||
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.`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -47,6 +47,7 @@ function Aside() {
|
|||
<Link to="/acgm">随机动漫音乐</Link>
|
||||
<Link to="/bili">哔哩哔哩小窗</Link>
|
||||
<Link to="/bing">必应每日壁纸</Link>
|
||||
{profile.name && <Link to="/admin">轻管理</Link>}
|
||||
</nav>
|
||||
</nav>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
// React
|
||||
import React, { useEffect, useLayoutEffect } from "react";
|
||||
import useGlobalData from "@/hooks/useGlobalData";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
|
||||
// Components
|
||||
import GlobalContext from "@/components/GlobalContext";
|
||||
import Aside from "@/components/Layout/Aside";
|
||||
import Footer from "@/components/Layout/Footer";
|
||||
|
||||
|
|
@ -20,7 +18,6 @@ interface FrontWrapperProps {
|
|||
// Components
|
||||
function FrontWrapper(props: FrontWrapperProps) {
|
||||
const location = useLocation();
|
||||
let [globalData, setGlobalData] = useGlobalData();
|
||||
|
||||
useEffect(() => {
|
||||
const name = import.meta.env.PAUL_SITENAME;
|
||||
|
|
@ -38,11 +35,11 @@ function FrontWrapper(props: FrontWrapperProps) {
|
|||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={{ globalData, setGlobalData }}>
|
||||
<>
|
||||
<Aside />
|
||||
{props.element}
|
||||
<Footer />
|
||||
</GlobalContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -52,6 +52,39 @@ blockquote.notice p{
|
|||
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{
|
||||
row-gap: .5em;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import React from "react";
|
||||
import React, { lazy } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import useGlobalData from "@/hooks/useGlobalData";
|
||||
import GlobalContext from "@/components/GlobalContext";
|
||||
|
||||
import "./kico.css";
|
||||
import "./index.css";
|
||||
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
|
||||
|
||||
|
||||
// Routes
|
||||
import Home from "./pages/index";
|
||||
import Login from "./pages/login";
|
||||
|
|
@ -23,11 +25,16 @@ import NoMatch from "./pages/404";
|
|||
|
||||
// RouteWrapper
|
||||
import FrontWrapper from "./components/Layout/FrontWrapper";
|
||||
import FrontWrapperAuth from "./components/Layout/FrontWrapperAuth";
|
||||
|
||||
const admin = lazy(() => import("./pages/admin"));
|
||||
|
||||
// Components
|
||||
function App() {
|
||||
let [globalData, setGlobalData] = useGlobalData();
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={{ globalData, setGlobalData }}>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={
|
||||
|
|
@ -36,6 +43,9 @@ function App() {
|
|||
<Route path="/login" element={
|
||||
<FrontWrapper title="登录" element={<Login />} /> }
|
||||
/>
|
||||
<Route path="/admin" element={
|
||||
<FrontWrapperAuth title="轻管理" element={admin} /> }
|
||||
/>
|
||||
<Route path="/notice" element={
|
||||
<FrontWrapper title="使用约定" element={<Notice />} /> }
|
||||
/>
|
||||
|
|
@ -62,6 +72,7 @@ function App() {
|
|||
/>
|
||||
</Routes>
|
||||
</Router>
|
||||
</GlobalContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -31,12 +31,17 @@ function Login() {
|
|||
onSuccess: (data) => {
|
||||
console.log(data);
|
||||
|
||||
if (data.data) {
|
||||
setGlobalData({
|
||||
type: "profile",
|
||||
value: data.profile
|
||||
value: data.data.profile
|
||||
});
|
||||
|
||||
navigate("/")
|
||||
navigate("/");
|
||||
}
|
||||
else {
|
||||
alert("错误的用户名或密码");
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
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(
|
||||
Get(),
|
||||
|
|
@ -11,12 +12,36 @@ export default Api(
|
|||
|
||||
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, {
|
||||
maxAge: 60 * 60 * 24 * 2,
|
||||
maxAge: 60 * 60 * 24 * 2 * 100,
|
||||
httpOnly: true
|
||||
});
|
||||
|
||||
// 标记已登录
|
||||
ctx.cookies.set("paul-logined", 1, {
|
||||
maxAge: 60 * 60 * 24 * 2 * 100,
|
||||
httpOnly: false
|
||||
});
|
||||
|
||||
return {
|
||||
code: 1,
|
||||
msg: "Success",
|
||||
data: {
|
||||
profile: {
|
||||
name: "Paul",
|
||||
avatar: "https://sdn.geekzu.org/avatar/d22eb460ecab37fcd7205e6a3c55c228?s=200&r=X&d=",
|
||||
|
|
@ -24,4 +49,5 @@ export default Api(
|
|||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { useContext } from "@midwayjs/hooks";
|
||||
import { CustomStrategy, PassportStrategy } from '@midwayjs/passport';
|
||||
import { Strategy, ExtractJwt } from 'passport-jwt';
|
||||
import { Config } from '@midwayjs/decorator';
|
||||
import { CustomStrategy, PassportStrategy } from "@midwayjs/passport";
|
||||
import { Strategy, ExtractJwt } from "passport-jwt";
|
||||
import { Config } from "@midwayjs/decorator";
|
||||
|
||||
@CustomStrategy()
|
||||
export class JwtStrategy extends PassportStrategy(
|
||||
Strategy,
|
||||
'jwt'
|
||||
"jwt"
|
||||
) {
|
||||
@Config('jwt')
|
||||
jwtConfig;
|
||||
@Config("jwt")
|
||||
jwtConfig: any;
|
||||
|
||||
async validate(payload) {
|
||||
console.log('payload', payload);
|
||||
console.log("payload", payload);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue