mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
feat(server): manage auth cookies (#8317)
This commit is contained in:
@@ -172,16 +172,19 @@ export class AuthController {
|
||||
});
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('/sign-out')
|
||||
async signOut(
|
||||
@Res() res: Response,
|
||||
@Session() session: Session,
|
||||
@Body() { all }: { all: boolean }
|
||||
@Session() session: Session | undefined,
|
||||
@Query('user_id') userId: string | undefined
|
||||
) {
|
||||
await this.auth.signOut(
|
||||
session.sessionId,
|
||||
all ? undefined : session.userId
|
||||
);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.auth.signOut(session.sessionId, userId);
|
||||
await this.auth.refreshCookies(res, session.sessionId);
|
||||
|
||||
res.status(HttpStatus.OK).send({});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
} from '@nestjs/common';
|
||||
import { Injectable, SetMetadata } from '@nestjs/common';
|
||||
import { ModuleRef, Reflector } from '@nestjs/core';
|
||||
import type { Request } from 'express';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import {
|
||||
AuthenticationRequired,
|
||||
@@ -37,7 +37,7 @@ export class AuthGuard implements CanActivate, OnModuleInit {
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const { req, res } = getRequestResponseFromContext(context);
|
||||
|
||||
const userSession = await this.signIn(req);
|
||||
const userSession = await this.signIn(req, res);
|
||||
if (res && userSession && userSession.expiresAt) {
|
||||
await this.auth.refreshUserSessionIfNeeded(res, userSession);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export class AuthGuard implements CanActivate, OnModuleInit {
|
||||
return true;
|
||||
}
|
||||
|
||||
async signIn(req: Request): Promise<Session | null> {
|
||||
async signIn(req: Request, res?: Response): Promise<Session | null> {
|
||||
if (req.session) {
|
||||
return req.session;
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export class AuthGuard implements CanActivate, OnModuleInit {
|
||||
parseCookies(req);
|
||||
|
||||
// TODO(@forehalo): a cache for user session
|
||||
const userSession = await this.auth.getUserSessionFromRequest(req);
|
||||
const userSession = await this.auth.getUserSessionFromRequest(req, res);
|
||||
|
||||
if (userSession) {
|
||||
req.session = {
|
||||
|
||||
@@ -122,35 +122,45 @@ export class AuthService implements OnApplicationBootstrap {
|
||||
sessionId: string,
|
||||
userId?: string
|
||||
): Promise<{ user: CurrentUser; session: UserSession } | null> {
|
||||
const userSession = await this.db.userSession.findFirst({
|
||||
const sessions = await this.getUserSessions(sessionId);
|
||||
|
||||
if (!sessions.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let userSession: UserSession | undefined;
|
||||
|
||||
// try read from user provided cookies.userId
|
||||
if (userId) {
|
||||
userSession = sessions.find(s => s.userId === userId);
|
||||
}
|
||||
|
||||
// fallback to the first valid session if user provided userId is invalid
|
||||
if (!userSession) {
|
||||
// checked
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
userSession = sessions.at(-1)!;
|
||||
}
|
||||
|
||||
const user = await this.user.findUserById(userSession.userId);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { user: sessionUser(user), session: userSession };
|
||||
}
|
||||
|
||||
async getUserSessions(sessionId: string) {
|
||||
return this.db.userSession.findMany({
|
||||
where: {
|
||||
sessionId,
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
sessionId: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
expiresAt: true,
|
||||
user: true,
|
||||
OR: [{ expiresAt: { gt: new Date() } }, { expiresAt: null }],
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
// no such session
|
||||
if (!userSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// user session expired
|
||||
if (userSession.expiresAt && userSession.expiresAt <= new Date()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { user: sessionUser(userSession.user), session: userSession };
|
||||
}
|
||||
|
||||
async createUserSession(
|
||||
@@ -309,6 +319,25 @@ export class AuthService implements OnApplicationBootstrap {
|
||||
this.setUserCookie(res, userId);
|
||||
}
|
||||
|
||||
async refreshCookies(res: Response, sessionId?: string) {
|
||||
if (sessionId) {
|
||||
const users = await this.getUserList(sessionId);
|
||||
const candidateUser = users.at(-1);
|
||||
|
||||
if (candidateUser) {
|
||||
this.setUserCookie(res, candidateUser.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.clearCookies(res);
|
||||
}
|
||||
|
||||
private clearCookies(res: Response<any, Record<string, any>>) {
|
||||
res.clearCookie(AuthService.sessionCookieName);
|
||||
res.clearCookie(AuthService.userCookieName);
|
||||
}
|
||||
|
||||
setUserCookie(res: Response, userId: string) {
|
||||
res.cookie(AuthService.userCookieName, userId, {
|
||||
...this.cookieOptions,
|
||||
@@ -319,14 +348,28 @@ export class AuthService implements OnApplicationBootstrap {
|
||||
});
|
||||
}
|
||||
|
||||
async getUserSessionFromRequest(req: Request) {
|
||||
async getUserSessionFromRequest(req: Request, res?: Response) {
|
||||
const { sessionId, userId } = this.getSessionOptionsFromRequest(req);
|
||||
|
||||
if (!sessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getUserSession(sessionId, userId);
|
||||
const session = await this.getUserSession(sessionId, userId);
|
||||
|
||||
if (res) {
|
||||
if (session) {
|
||||
// set user id cookie for fast authentication
|
||||
if (!userId || userId !== session.user.id) {
|
||||
this.setUserCookie(res, session.user.id);
|
||||
}
|
||||
} else if (sessionId) {
|
||||
// clear invalid cookies.session and cookies.userId
|
||||
this.clearCookies(res);
|
||||
}
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
async changePassword(
|
||||
|
||||
Reference in New Issue
Block a user