中间件
session.schema.ts
新建src/session/session.schema.ts。
import { Prop } from "../schema.ts";
import { User } from "../user/user.schema.ts";
export const SESSION_KEY = "session-id";
export class Session {
@Prop()
userId?: string;
@Prop()
success?: string;
@Prop()
error?: string;
user?: User | null;
}
从上文知道,SESSION_KEY可以是随机的,任何一个固定的字符串都可以,当然,网络协议默认的字段除外。这里的值你可以自行修改。
一个session就是一个会话,我们的通知功能依赖于此,包含了用户id(userId)、成功提示信息(success)、错误提示信息(error)这三个字段,而且这三个都不是一个session必需的。
session.interface.ts
新建src/session/session.interface.ts
import { User } from "../user/user.schema.ts";
export interface CreateSession {
userId?: string;
success?: string;
error?: string;
}
export interface UpdateSession {
id: string;
userId?: string;
success?: string;
error?: string;
}
export interface ISession {
id: string;
user?: User;
success?: string;
error?: string;
}
session.service.ts
新建src/session/session.service.ts,提供增删改查4个方法。
import { Injectable } from "oak_nest";
import { InjectModel, Model } from "../model.ts";
import { Session } from "./session.schema.ts";
import type { CreateSession, UpdateSession } from "./session.interface.ts";
import { UserService } from "../user/user.service.ts";
@Injectable()
export class SessionService {
constructor(
@InjectModel(Session) private readonly model: Model<Session>,
private readonly userService: UserService,
) {
}
async save(params: CreateSession): Promise<string> {
const id = await this.model.insertOne(params);
return id.toString();
}
async findById(id: string, isWithUserInfo: boolean) {
const session = await this.model.findById(id);
if (!session) {
return;
}
if (isWithUserInfo && session.userId) {
session.user = await this.userService.getUserById(session.userId);
}
return session;
}
update(params: UpdateSession) {
return this.model.findByIdAndUpdate(params.id, params);
}
deleteById(id: string) {
return this.model.findByIdAndDelete(id);
}
findAll(): Promise<Session[]> {
return this.model.findAll();
}
}
session.decorator.ts
新建src/session/session.decorator.ts,提供一个Flash装饰器。
import { Context, createParamDecorator } from "oak_nest";
import type { CreateSession } from "./session.interface.ts";
export const Flash = createParamDecorator(
(context: Context): Flash => {
return (key, val) => {
context.state[key] = val;
};
},
);
export type Flash = (
key: keyof CreateSession,
val: string | boolean | number,
) => void;
context.state是oak框架保留的一个object类型的字段,它的信息会随着中间件的流转而逐层传递。
session.middleware.ts
新建src/session/session.middleware.ts,也就是中间件:
import { assert, Context, Factory, Next } from "oak_nest";
import { SESSION_KEY } from "./session.schema.ts";
import { SessionService } from "./session.service.ts";
export async function SessionMiddleware(context: Context, next: Next) {
const sessionService = await Factory(SessionService);
let sessionId = await context.cookies.get(SESSION_KEY);
let session;
if (sessionId) {
session = await sessionService.findById(sessionId, true);
if (!session) {
console.warn(`没有找到session: ${sessionId}`);
}
} else {
console.warn(`cookie中没有找到${SESSION_KEY}`);
}
if (!session) {
sessionId = await sessionService.save({});
console.log(`创建session: ${sessionId}`);
session = {
id: sessionId,
};
}
context.state.session = session;
context.state.locals = { // 重点是这一句,把session信息赋给state.locals
success: session.success,
error: session.error,
user: session.user,
};
console.log("session:", session);
await next();
assert(sessionId);
await context.cookies.set(SESSION_KEY, sessionId);
// 处理flash的消息,或者清理信息
const { success, error, userId } = context.state;
if (success || error || userId !== undefined) {
await sessionService.update({
id: sessionId,
success,
error,
userId,
});
} else {
if (session.error || session.success) { // 通知消息只显示一次
await sessionService.update({
id: sessionId,
error: "",
success: "",
});
}
}
}
通知主要靠这一篇实现,我们之前创建的views/components/notification.ejs用到了success和error:
核心思想是在调用接口前(next()方法上面的代码)获取session或者创建session,而在调用接口之后,将sessionId存到cookie里,返回给前端,最后更新或清理session信息。
user.controller.ts
修改src/user/user.controller.ts,只需要把上次的两个TODO实现,调用flash函数,设置state状态。
import { Flash } from "../session/session.decorator.ts";
import {
Context,
Controller,
Delete,
Get,
Params,
Post,
Res,
Response,
REDIRECT_BACK,
FormData,
validateParams,
} from "oak_nest";
import type { FormDataFormattedBody } from "oak_nest";
@Controller("/")
export class UserController {
@Post("signup")
async signup(
@FormData() params: FormDataFormattedBody<CreateUserDto>,
@Res() res: Response,
@Flash() flash: Flash,
) {
try {
... //原来代码不变
// 提示注册成功
flash("success", "注册成功");
flash("userId", id);
res.redirect("/posts");
} catch (e) {
// 提示错误
flash("error", e.message);
this.logger.error(e.message);
res.redirect(REDIRECT_BACK);
}
}
@Get("currentUserInfo")
currentUserInfo(context: Context) {
const user = { ...context.state.locals?.user };
delete user.password;
return user;
}
}
main.ts
修改src/main.ts,在异常中间件后面使用session中间件:
import { SessionMiddleware } from "./session/session.middleware.ts";
const app = await NestFactory.create(AppModule);
... 异常中间件
app.use(SessionMiddleware);
验证
清理cookie,再次访问http://localhost:8000/signup,再注册一个用户。
打开右上角设置,能看到已经变成已登陆状态。
在命令窗口的控制台也能看到打印的session信息。
访问http://localhost:8000/currentUserInfo,能看到打印的用户信息:
再回到注册页面,特意输入不一样的密码,提交,看提示:
再刷新页面,能看到通知信息没有了。
到此,我们这个通知就基本上成功了。
作业
思考下,目前我们的session还有什么缺陷?应该怎么优化?