Feat: Add Passport + JWT
增加 Passport 和 JWT 验证支持(目前为假验证)前端登录页面,部分页面内容和样式优化
This commit is contained in:
parent
b1a9e09396
commit
7823572503
|
|
@ -10,7 +10,9 @@
|
|||
"dependencies": {
|
||||
"@midwayjs/hooks": "^3.0.0",
|
||||
"@midwayjs/hooks-kit": "^3.0.0",
|
||||
"@midwayjs/jwt": "^3.3.5",
|
||||
"@midwayjs/koa": "^3.3.0",
|
||||
"@midwayjs/passport": "^3.3.5",
|
||||
"@midwayjs/redis": "^3.3.2",
|
||||
"@midwayjs/rpc": "^3.0.0",
|
||||
"@prisma/client": "^3.12.0",
|
||||
|
|
@ -18,6 +20,8 @@
|
|||
"dotenv": "^16.0.0",
|
||||
"isomorphic-unfetch": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"passport": "^0.5.2",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"prismjs": "^1.27.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
|
@ -27,6 +31,7 @@
|
|||
"@midwayjs/mock": "^3.3.0",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/lodash": "^4.14.181",
|
||||
"@types/passport-jwt": "^3.0.6",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react": "^17.0.44",
|
||||
"@types/react-dom": "^17.0.15",
|
||||
|
|
|
|||
|
|
@ -26,15 +26,15 @@ export const Donate = () => {
|
|||
<h3>赞助我:</h3>
|
||||
<p>本服务将长期免费提供,运行至今暂无任何打赏记录,故不进行展示。如您能为本站献上微薄之力,就是对我的最大支持了~ 记得备注「支持保罗的 API」噢!</p>
|
||||
<div className="row center">
|
||||
<div className="col-6 col-m-4 center-fixed">
|
||||
<div className="col-4 center-fixed">
|
||||
<img src={Alipay} alt="支付宝" />
|
||||
<p>支付宝</p>
|
||||
</div>
|
||||
<div className="col-6 col-m-4 center-fixed">
|
||||
<div className="col-4 center-fixed">
|
||||
<img src={WeChat} alt="微信支付" />
|
||||
<p>微信支付</p>
|
||||
</div>
|
||||
<div className="col-6 col-m-4 center-fixed">
|
||||
<div className="col-4 center-fixed">
|
||||
<img src={QQ} alt="QQ 钱包" />
|
||||
<p>QQ 钱包</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import { createContext } from "react";
|
||||
|
||||
import { IGlobalData } from "@/types/GlobalData";
|
||||
|
||||
import { initalState, IAction } from "@/hooks/useGlobalData";
|
||||
|
||||
const GlobalContext = createContext<{ globalData: IGlobalData, setGlobalData: React.Dispatch<IAction> }>({
|
||||
globalData: initalState,
|
||||
setGlobalData() {}
|
||||
});
|
||||
|
||||
GlobalContext.displayName = "GlobalContext";
|
||||
|
||||
export default GlobalContext;
|
||||
|
|
@ -1,25 +1,36 @@
|
|||
// React
|
||||
import React, { useState } from "react";
|
||||
import useGlobalContext from "@/hooks/useGlobalContext";
|
||||
|
||||
|
||||
// UI
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import Avatar from "../../images/avatar.jpg";
|
||||
import Avatar from "@/images/avatar.jpg";
|
||||
|
||||
|
||||
// Interface
|
||||
import { MouseEvent } from "react";
|
||||
|
||||
|
||||
// Components
|
||||
function Aside() {
|
||||
|
||||
const { globalData: { profile } } = useGlobalContext();
|
||||
const [sideOpen, setSideOpen] = useState(false);
|
||||
|
||||
const toggleClick = () => {
|
||||
const onMenuClick = (ev: MouseEvent<HTMLElement>) => {
|
||||
let el = ev.target as HTMLElement;
|
||||
|
||||
el.tagName === "A" && setSideOpen(!sideOpen);
|
||||
}
|
||||
|
||||
const onToggleClick = () => {
|
||||
setSideOpen(!sideOpen);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className={sideOpen ? "sidebar active" : "sidebar"}>
|
||||
<div className="toggle" onClick={toggleClick}></div>
|
||||
<aside className={sideOpen ? "sidebar active" : "sidebar"} onClick={onMenuClick}>
|
||||
<div className="toggle" onClick={onToggleClick}></div>
|
||||
<div className="wrapper">
|
||||
<div className="header">
|
||||
<h1>保罗|API</h1>
|
||||
|
|
@ -40,8 +51,10 @@ function Aside() {
|
|||
</nav>
|
||||
</nav>
|
||||
<div className="user-area no-login">
|
||||
<img src={Avatar} alt="头像" />
|
||||
<span className="username">未登录</span>
|
||||
<Link to="/login">
|
||||
<img src={profile.avatar || Avatar} alt="头像" />
|
||||
<span className="username">{profile.name || "未登录"}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
// 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";
|
||||
|
||||
|
|
@ -18,6 +20,7 @@ interface FrontWrapperProps {
|
|||
// Components
|
||||
function FrontWrapper(props: FrontWrapperProps) {
|
||||
const location = useLocation();
|
||||
let [globalData, setGlobalData] = useGlobalData();
|
||||
|
||||
useEffect(() => {
|
||||
const name = import.meta.env.PAUL_SITENAME;
|
||||
|
|
@ -35,11 +38,11 @@ function FrontWrapper(props: FrontWrapperProps) {
|
|||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalContext.Provider value={{ globalData, setGlobalData }}>
|
||||
<Aside />
|
||||
{props.element}
|
||||
<Footer />
|
||||
</>
|
||||
</GlobalContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import GlobalContext from "@/components/GlobalContext";
|
||||
|
||||
function useGlobalContext() {
|
||||
return useContext(GlobalContext);
|
||||
}
|
||||
|
||||
export default useGlobalContext;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { useReducer } from "react";
|
||||
|
||||
import { IGlobalData } from "@/types/GlobalData";
|
||||
|
||||
export type IAction =
|
||||
| { type: "all", value: IGlobalData }
|
||||
| { type: "profile", value: Partial<IGlobalData["profile"]> }
|
||||
|
||||
export const initalState: IGlobalData = {
|
||||
profile: {
|
||||
name: "",
|
||||
avatar: ""
|
||||
}
|
||||
};
|
||||
|
||||
function reducer(state: IGlobalData, action: IAction): IGlobalData {
|
||||
if (!action) return state;
|
||||
|
||||
if (action.type === "all") {
|
||||
return {
|
||||
...state,
|
||||
...action.value
|
||||
};
|
||||
}
|
||||
else if (action.type === "profile") {
|
||||
return {
|
||||
...state,
|
||||
profile: {
|
||||
...state.profile,
|
||||
...action.value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function useGlobalData(): [IGlobalData, React.Dispatch<IAction>]{
|
||||
let [state, dispatch] = useReducer(reducer, initalState);
|
||||
|
||||
return [state, dispatch];
|
||||
}
|
||||
|
||||
export default useGlobalData;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { useState } from "react";
|
||||
|
||||
export interface IParamsOption {
|
||||
defaultValue?: Record<string, any>;
|
||||
removeBlank?: boolean
|
||||
}
|
||||
|
||||
function useStat<T extends Record<string, any>>(defaultValue: T, options?: IParamsOption | undefined): [T, ((newState: Partial<T>) => T), (value: React.SetStateAction<T>) => void];
|
||||
|
||||
// 适合组件状态设置的归档和存储,不可还原,需要还原成初始字段可使用 useParams
|
||||
function useStat(initValue = {}, options?: IParamsOption | undefined) {
|
||||
const [state, setState] = useState(initValue);
|
||||
|
||||
// 删除没用的
|
||||
const removeUndefined = (obj: Record<string, any>) => {
|
||||
const _keys = Object.keys(obj);
|
||||
|
||||
_keys.forEach(item => {
|
||||
if (options?.removeBlank) {
|
||||
(obj[item] === undefined || obj[item] === "") && delete obj[item];
|
||||
}
|
||||
else {
|
||||
obj[item] === undefined && delete obj[item];
|
||||
}
|
||||
})
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 覆盖部分值
|
||||
const setStat = (newState: Record<string, any> = {}) => {
|
||||
setState(prevState => removeUndefined({ ...prevState, ...newState }));
|
||||
}
|
||||
|
||||
return [state, setStat, setState];
|
||||
}
|
||||
|
||||
export default useStat;
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Paul API
|
||||
# By: Dreamer-Paul
|
||||
# Last Update: 2021.12.16
|
||||
# Last Update: 2022.5.3
|
||||
|
||||
保罗的首页、作品和 API 页通用模板
|
||||
|
||||
|
|
@ -108,6 +108,7 @@ button{
|
|||
color: #fff;
|
||||
width: 10em;
|
||||
position: fixed;
|
||||
user-select: none;
|
||||
background-color: #3498db;
|
||||
background-color: var(--primary);
|
||||
transition: transform .3s;
|
||||
|
|
@ -146,6 +147,7 @@ button{
|
|||
}
|
||||
.sidebar nav i{
|
||||
opacity: .8;
|
||||
pointer-events: none;
|
||||
transition: transform .3s;
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +160,7 @@ button{
|
|||
}
|
||||
.sidebar a.active:before ,.sidebar .has-child > a:before{
|
||||
right: 0;
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
border: .75em solid transparent;
|
||||
border-right-color: #fff;
|
||||
|
|
@ -177,22 +179,28 @@ button{
|
|||
box-shadow: .25em 0 .5em rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.sidebar a.active:before ,.sidebar .has-child > a:before{
|
||||
content: none;
|
||||
}
|
||||
|
||||
aside .toggle{
|
||||
top: 0;
|
||||
left: 10em;
|
||||
z-index: 3;
|
||||
color: #fff;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
|
||||
z-index: 3;
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
line-height: 3em;
|
||||
text-align: center;
|
||||
background-color: var(--primary);
|
||||
box-shadow: .25em 0 .5em rgba(0, 0, 0, .2);
|
||||
}
|
||||
aside .toggle:before{
|
||||
content: "\f0c9";
|
||||
line-height: 2.5em;
|
||||
font-size: 1.2em;
|
||||
font-family: "FontAwesome";
|
||||
}
|
||||
|
||||
|
|
@ -207,20 +215,28 @@ button{
|
|||
}
|
||||
|
||||
.sidebar .user-area{
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: .75em 1em;
|
||||
transition: background .3s;
|
||||
border-top: 1px solid rgba(0, 0, 0, .15);
|
||||
transition: background-color .3s;
|
||||
background-color: rgba(0, 0, 0, .1);
|
||||
}
|
||||
.sidebar .user-area:hover{ background: rgba(0, 0, 0, .2) }
|
||||
.sidebar .user-area:hover{ background-color: rgba(0, 0, 0, .2) }
|
||||
|
||||
.sidebar .user-area a{
|
||||
color: inherit;
|
||||
display: block;
|
||||
padding: .75em 1em;
|
||||
}
|
||||
|
||||
.sidebar .user-area img{
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
object-fit: cover;
|
||||
border-radius: 66%;
|
||||
pointer-events: none;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
.sidebar .user-area .username{
|
||||
margin-left: .5em;
|
||||
pointer-events: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
|||
|
||||
// Routes
|
||||
import Home from "./pages/index";
|
||||
import Login from "./pages/login";
|
||||
import Notice from "./pages/notice";
|
||||
import Log from "./pages/log";
|
||||
import Netease from "./pages/netease";
|
||||
|
|
@ -32,6 +33,9 @@ function App() {
|
|||
<Route path="/" element={
|
||||
<FrontWrapper element={<Home />} />}
|
||||
/>
|
||||
<Route path="/login" element={
|
||||
<FrontWrapper title="登录" element={<Login />} /> }
|
||||
/>
|
||||
<Route path="/notice" element={
|
||||
<FrontWrapper title="使用约定" element={<Notice />} /> }
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const apiMap: Record<string, string> = {
|
|||
|
||||
function Index() {
|
||||
|
||||
const { data: stat } = useRequest(() => getStat());
|
||||
const { data: stat } = useRequest(getStat);
|
||||
|
||||
return (
|
||||
<main>
|
||||
|
|
@ -87,7 +87,6 @@ function Index() {
|
|||
<About />
|
||||
</article>
|
||||
</main>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
// React
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useRequest } from "ahooks";
|
||||
import useStat from "@/hooks/useStat";
|
||||
import useGlobalContext from "@/hooks/useGlobalContext";
|
||||
|
||||
|
||||
// UI
|
||||
import ArticleHead from "@/components/Layout/ArticleHead";
|
||||
|
||||
|
||||
// Tool
|
||||
import loginRequest from "../server/api/auth/login";
|
||||
|
||||
|
||||
// Interface
|
||||
import { MouseEvent, ChangeEvent } from "react";
|
||||
|
||||
|
||||
// Components
|
||||
function Login() {
|
||||
const navigate = useNavigate();
|
||||
const { globalData, setGlobalData } = useGlobalContext();
|
||||
const { loading, run } = useRequest(
|
||||
(params) => loginRequest({
|
||||
query: params
|
||||
}),
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
console.log(data);
|
||||
|
||||
setGlobalData({
|
||||
type: "profile",
|
||||
value: data.profile
|
||||
});
|
||||
|
||||
navigate("/")
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const [input, setInput] = useStat({
|
||||
username: "",
|
||||
password: ""
|
||||
});
|
||||
|
||||
const onSubmit = (ev: MouseEvent<HTMLButtonElement>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
run(input);
|
||||
|
||||
console.log("ok");
|
||||
}
|
||||
|
||||
const onInputChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
setInput({ [ev.target.name]: ev.target.value });
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<ArticleHead title="登录" desc="目前来说,登录是没有用的" />
|
||||
<form>
|
||||
<fieldset>
|
||||
<label>
|
||||
<input type="text" name="username" placeholder="用户名"
|
||||
value={input.username} onChange={onInputChange}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<input type="password" name="password" placeholder="密码"
|
||||
value={input.password} onChange={onInputChange}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<button className="btn green" onClick={onSubmit}>
|
||||
{
|
||||
loading ? <i className="fa fa-spinner fa-spin"></i> : "登录"
|
||||
}
|
||||
</button>
|
||||
</label>
|
||||
</fieldset>
|
||||
</form>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { Api, Get, Query, useContext, useInject } from "@midwayjs/hooks";
|
||||
import { RedisService } from "@midwayjs/redis";
|
||||
import { prisma } from "../../utils/prisma";
|
||||
import { prisma } from "../utils/prisma";
|
||||
|
||||
import { getSong } from "../../utils/netease";
|
||||
import { getSong } from "../utils/netease";
|
||||
|
||||
export default Api(
|
||||
Get(),
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
import { Api, Post, Query, useContext, useInject } from "@midwayjs/hooks";
|
||||
import { Api, Post, Query, useContext, useInject, Middleware } from "@midwayjs/hooks";
|
||||
import { RedisService } from "@midwayjs/redis";
|
||||
import { prisma } from "../../utils/prisma";
|
||||
import { JwtPassportMiddleware } from "../../../middleware/jwt.middleware";
|
||||
|
||||
import { getSong } from "../../utils/netease";
|
||||
import { prisma } from "../../../utils/prisma";
|
||||
|
||||
import { getSong } from "../../../utils/netease";
|
||||
|
||||
export default Api(
|
||||
Post(),
|
||||
Middleware(JwtPassportMiddleware),
|
||||
Query<{ id: string, bangumi: string }>(),
|
||||
async () => {
|
||||
const ctx = useContext();
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { Api, Get, Query, useContext, useInject } from "@midwayjs/hooks";
|
||||
import { JwtService } from '@midwayjs/jwt';
|
||||
|
||||
export default Api(
|
||||
Get(),
|
||||
Query<{ username: string, password: string }>(),
|
||||
async () => {
|
||||
const ctx = useContext();
|
||||
|
||||
const jwt = await useInject(JwtService);
|
||||
|
||||
const token = await jwt.sign({ name: "Paul" });
|
||||
|
||||
ctx.cookies.set("paul-token", token, {
|
||||
maxAge: 60 * 60 * 24 * 2,
|
||||
httpOnly: true
|
||||
});
|
||||
|
||||
return {
|
||||
profile: {
|
||||
name: "Paul",
|
||||
avatar: "https://sdn.geekzu.org/avatar/d22eb460ecab37fcd7205e6a3c55c228?s=200&r=X&d=",
|
||||
},
|
||||
token
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -2,6 +2,8 @@ import { createConfiguration, hooks } from "@midwayjs/hooks";
|
|||
import * as Koa from "@midwayjs/koa";
|
||||
import * as dotenv from "dotenv";
|
||||
import * as redis from "@midwayjs/redis";
|
||||
import * as jwt from "@midwayjs/jwt";
|
||||
import * as passport from "@midwayjs/passport";
|
||||
|
||||
const env = dotenv.config();
|
||||
|
||||
|
|
@ -9,7 +11,9 @@ const env = dotenv.config();
|
|||
* setup midway server
|
||||
*/
|
||||
export default createConfiguration({
|
||||
imports: [Koa, redis, hooks()],
|
||||
imports: [
|
||||
Koa, redis, hooks(), jwt, passport
|
||||
],
|
||||
importConfigs: [{
|
||||
default: {
|
||||
keys: "session_keys",
|
||||
|
|
@ -20,6 +24,13 @@ export default createConfiguration({
|
|||
db: 0,
|
||||
},
|
||||
},
|
||||
jwt: {
|
||||
secret: "Test000666hhhh",
|
||||
expiresIn: "5h",
|
||||
},
|
||||
passport: {
|
||||
session: false
|
||||
}
|
||||
}
|
||||
}],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { Middleware } from "@midwayjs/decorator";
|
||||
import { PassportMiddleware } from "@midwayjs/passport";
|
||||
import { JwtStrategy } from "../strategy/jwt.strategy";
|
||||
import * as passport from "passport";
|
||||
import { NextFunction, Context } from '@midwayjs/koa';
|
||||
|
||||
@Middleware()
|
||||
export class JwtPassportMiddleware extends PassportMiddleware(JwtStrategy) {
|
||||
getAuthenticateOptions(): Promise<passport.AuthenticateOptions> | passport.AuthenticateOptions {
|
||||
return {};
|
||||
}
|
||||
|
||||
// resolve() {
|
||||
// return async (ctx: Context, next: NextFunction) => {
|
||||
// const result = await next();
|
||||
|
||||
// console.log(result);
|
||||
|
||||
// return {
|
||||
// code: 0,
|
||||
// msg: '111',
|
||||
// data: result,
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
static getName(): string {
|
||||
return "auth";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { useContext } from "@midwayjs/hooks";
|
||||
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'
|
||||
) {
|
||||
@Config('jwt')
|
||||
jwtConfig;
|
||||
|
||||
async validate(payload) {
|
||||
console.log('payload', payload);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
getStrategyOptions(): any {
|
||||
const ctx = useContext();
|
||||
|
||||
console.log(ctx.headers, ctx.cookies.get("paul-token"));
|
||||
|
||||
return {
|
||||
secretOrKey: this.jwtConfig.secret,
|
||||
jwtFromRequest: () => ctx.cookies.get("paul-token") //ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export interface IGlobalData {
|
||||
profile: {
|
||||
name: string
|
||||
avatar: string
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue