mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(server): auto refresh session (#6613)
This commit is contained in:
@@ -36,7 +36,7 @@ export class AuthGuard implements CanActivate, OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext) {
|
async canActivate(context: ExecutionContext) {
|
||||||
const { req } = getRequestResponseFromContext(context);
|
const { req, res } = getRequestResponseFromContext(context);
|
||||||
|
|
||||||
// check cookie
|
// check cookie
|
||||||
let sessionToken: string | undefined =
|
let sessionToken: string | undefined =
|
||||||
@@ -51,7 +51,19 @@ export class AuthGuard implements CanActivate, OnModuleInit {
|
|||||||
req.headers[AuthService.authUserSeqHeaderName]
|
req.headers[AuthService.authUserSeqHeaderName]
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = await this.auth.getUser(sessionToken, userSeq);
|
const { user, expiresAt } = await this.auth.getUser(
|
||||||
|
sessionToken,
|
||||||
|
userSeq
|
||||||
|
);
|
||||||
|
if (res && user && expiresAt) {
|
||||||
|
await this.auth.refreshUserSessionIfNeeded(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
sessionToken,
|
||||||
|
user.id,
|
||||||
|
expiresAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
req.sid = sessionToken;
|
req.sid = sessionToken;
|
||||||
|
|||||||
@@ -145,24 +145,27 @@ export class AuthService implements OnApplicationBootstrap {
|
|||||||
return sessionUser(user);
|
return sessionUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser(token: string, seq = 0): Promise<CurrentUser | null> {
|
async getUser(
|
||||||
|
token: string,
|
||||||
|
seq = 0
|
||||||
|
): Promise<{ user: CurrentUser | null; expiresAt: Date | null }> {
|
||||||
const session = await this.getSession(token);
|
const session = await this.getSession(token);
|
||||||
|
|
||||||
// no such session
|
// no such session
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return null;
|
return { user: null, expiresAt: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSession = session.userSessions.at(seq);
|
const userSession = session.userSessions.at(seq);
|
||||||
|
|
||||||
// no such user session
|
// no such user session
|
||||||
if (!userSession) {
|
if (!userSession) {
|
||||||
return null;
|
return { user: null, expiresAt: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// user session expired
|
// user session expired
|
||||||
if (userSession.expiresAt && userSession.expiresAt <= new Date()) {
|
if (userSession.expiresAt && userSession.expiresAt <= new Date()) {
|
||||||
return null;
|
return { user: null, expiresAt: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.db.user.findUnique({
|
const user = await this.db.user.findUnique({
|
||||||
@@ -170,10 +173,10 @@ export class AuthService implements OnApplicationBootstrap {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return { user: null, expiresAt: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessionUser(user);
|
return { user: sessionUser(user), expiresAt: userSession.expiresAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserList(token: string) {
|
async getUserList(token: string) {
|
||||||
@@ -269,6 +272,43 @@ export class AuthService implements OnApplicationBootstrap {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshUserSessionIfNeeded(
|
||||||
|
_req: Request,
|
||||||
|
res: Response,
|
||||||
|
sessionId: string,
|
||||||
|
userId: string,
|
||||||
|
expiresAt: Date,
|
||||||
|
ttr = this.config.auth.session.ttr
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (expiresAt && expiresAt.getTime() - Date.now() > ttr * 1000) {
|
||||||
|
// no need to refresh
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newExpiresAt = new Date(
|
||||||
|
Date.now() + this.config.auth.session.ttl * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.db.userSession.update({
|
||||||
|
where: {
|
||||||
|
sessionId_userId: {
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
expiresAt: newExpiresAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.cookie(AuthService.sessionCookieName, sessionId, {
|
||||||
|
expires: newExpiresAt,
|
||||||
|
...this.cookieOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async createUserSession(
|
async createUserSession(
|
||||||
user: { id: string },
|
user: { id: string },
|
||||||
existingSession?: string,
|
existingSession?: string,
|
||||||
|
|||||||
@@ -240,6 +240,13 @@ export interface AFFiNEConfig {
|
|||||||
* @default 15 days
|
* @default 15 days
|
||||||
*/
|
*/
|
||||||
ttl: number;
|
ttl: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application auth time to refresh in seconds
|
||||||
|
*
|
||||||
|
* @default 7 days
|
||||||
|
*/
|
||||||
|
ttr: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
|||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
ttl: 15 * ONE_DAY_IN_SEC,
|
ttl: 15 * ONE_DAY_IN_SEC,
|
||||||
|
ttr: 7 * ONE_DAY_IN_SEC,
|
||||||
},
|
},
|
||||||
accessToken: {
|
accessToken: {
|
||||||
ttl: 7 * ONE_DAY_IN_SEC,
|
ttl: 7 * ONE_DAY_IN_SEC,
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ test('should be able to visit public api if signed in', async t => {
|
|||||||
const { app, auth } = t.context;
|
const { app, auth } = t.context;
|
||||||
|
|
||||||
// @ts-expect-error mock
|
// @ts-expect-error mock
|
||||||
auth.getUser.resolves({ id: '1' });
|
auth.getUser.resolves({ user: { id: '1' } });
|
||||||
|
|
||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.get('/public')
|
.get('/public')
|
||||||
@@ -98,7 +98,7 @@ test('should be able to visit private api if signed in', async t => {
|
|||||||
const { app, auth } = t.context;
|
const { app, auth } = t.context;
|
||||||
|
|
||||||
// @ts-expect-error mock
|
// @ts-expect-error mock
|
||||||
auth.getUser.resolves({ id: '1' });
|
auth.getUser.resolves({ user: { id: '1' } });
|
||||||
|
|
||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.get('/private')
|
.get('/private')
|
||||||
@@ -111,6 +111,9 @@ test('should be able to visit private api if signed in', async t => {
|
|||||||
test('should be able to parse session cookie', async t => {
|
test('should be able to parse session cookie', async t => {
|
||||||
const { app, auth } = t.context;
|
const { app, auth } = t.context;
|
||||||
|
|
||||||
|
// @ts-expect-error mock
|
||||||
|
auth.getUser.resolves({ user: { id: '1' } });
|
||||||
|
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get('/public')
|
.get('/public')
|
||||||
.set('cookie', `${AuthService.sessionCookieName}=1`)
|
.set('cookie', `${AuthService.sessionCookieName}=1`)
|
||||||
@@ -122,6 +125,9 @@ test('should be able to parse session cookie', async t => {
|
|||||||
test('should be able to parse bearer token', async t => {
|
test('should be able to parse bearer token', async t => {
|
||||||
const { app, auth } = t.context;
|
const { app, auth } = t.context;
|
||||||
|
|
||||||
|
// @ts-expect-error mock
|
||||||
|
auth.getUser.resolves({ user: { id: '1' } });
|
||||||
|
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get('/public')
|
.get('/public')
|
||||||
.auth('1', { type: 'bearer' })
|
.auth('1', { type: 'bearer' })
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ test('should be able to get user from session', async t => {
|
|||||||
|
|
||||||
const session = await auth.createUserSession(u1);
|
const session = await auth.createUserSession(u1);
|
||||||
|
|
||||||
const user = await auth.getUser(session.sessionId);
|
const { user } = await auth.getUser(session.sessionId);
|
||||||
|
|
||||||
t.not(user, null);
|
t.not(user, null);
|
||||||
t.is(user!.id, u1.id);
|
t.is(user!.id, u1.id);
|
||||||
@@ -202,8 +202,8 @@ test('should be able to signout multi accounts session', async t => {
|
|||||||
|
|
||||||
t.not(signedOutSession, null);
|
t.not(signedOutSession, null);
|
||||||
|
|
||||||
const signedU2 = await auth.getUser(session.sessionId, 0);
|
const { user: signedU2 } = await auth.getUser(session.sessionId, 0);
|
||||||
const noUser = await auth.getUser(session.sessionId, 1);
|
const { user: noUser } = await auth.getUser(session.sessionId, 1);
|
||||||
|
|
||||||
t.is(noUser, null);
|
t.is(noUser, null);
|
||||||
t.not(signedU2, null);
|
t.not(signedU2, null);
|
||||||
@@ -215,6 +215,6 @@ test('should be able to signout multi accounts session', async t => {
|
|||||||
|
|
||||||
t.is(signedOutSession, null);
|
t.is(signedOutSession, null);
|
||||||
|
|
||||||
const noUser2 = await auth.getUser(session.sessionId, 0);
|
const { user: noUser2 } = await auth.getUser(session.sessionId, 0);
|
||||||
t.is(noUser2, null);
|
t.is(noUser2, null);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ test('should throw if oauth account already connected', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// @ts-expect-error mock
|
// @ts-expect-error mock
|
||||||
Sinon.stub(auth, 'getUser').resolves({ id: 'u2-id' });
|
Sinon.stub(auth, 'getUser').resolves({ user: { id: 'u2-id' } });
|
||||||
|
|
||||||
mockOAuthProvider(app, 'u2@affine.pro');
|
mockOAuthProvider(app, 'u2@affine.pro');
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ test('should be able to connect oauth account', async t => {
|
|||||||
const { app, u1, auth, db } = t.context;
|
const { app, u1, auth, db } = t.context;
|
||||||
|
|
||||||
// @ts-expect-error mock
|
// @ts-expect-error mock
|
||||||
Sinon.stub(auth, 'getUser').resolves({ id: u1.id });
|
Sinon.stub(auth, 'getUser').resolves({ user: { id: u1.id } });
|
||||||
|
|
||||||
mockOAuthProvider(app, u1.email);
|
mockOAuthProvider(app, u1.email);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user