Skip to content

Commit c68ef6d

Browse files
committed
refactor(server): use userSession model in auth service
1 parent ed66660 commit c68ef6d

File tree

7 files changed

+102
-124
lines changed

7 files changed

+102
-124
lines changed

packages/backend/server/src/__tests__/auth/guard.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ test.before(async t => {
4848
u1 = await auth.signUp('u1@affine.pro', '1');
4949

5050
const models = app.get(Models);
51-
const session = await models.session.create();
51+
const session = await models.session.createSession();
5252
sessionId = session.id;
5353
await auth.createUserSession(u1.id, sessionId);
5454

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { ScheduleModule } from '@nestjs/schedule';
2+
import { TestingModule } from '@nestjs/testing';
3+
import { PrismaClient } from '@prisma/client';
4+
import test from 'ava';
5+
6+
import { AuthModule, AuthService } from '../../core/auth';
7+
import { AuthCronJob } from '../../core/auth/job';
8+
import { createTestingModule } from '../utils';
9+
10+
let m: TestingModule;
11+
let db: PrismaClient;
12+
13+
test.before(async () => {
14+
m = await createTestingModule({
15+
imports: [ScheduleModule.forRoot(), AuthModule],
16+
});
17+
18+
db = m.get(PrismaClient);
19+
});
20+
21+
test.after.always(async () => {
22+
await m.close();
23+
});
24+
25+
test('should clean expired user sessions', async t => {
26+
const auth = m.get(AuthService);
27+
const job = m.get(AuthCronJob);
28+
const user1 = await auth.signUp('u1@affine.pro', '1');
29+
const user2 = await auth.signUp('u2@affine.pro', '1');
30+
await auth.createUserSession(user1.id);
31+
await auth.createUserSession(user2.id);
32+
let userSessions = await db.userSession.findMany();
33+
t.is(userSessions.length, 2);
34+
35+
// no expired sessions
36+
await job.cleanExpiredUserSessions();
37+
userSessions = await db.userSession.findMany();
38+
t.is(userSessions.length, 2);
39+
40+
// clean all expired sessions
41+
await db.userSession.updateMany({
42+
data: { expiresAt: new Date(Date.now() - 1000) },
43+
});
44+
await job.cleanExpiredUserSessions();
45+
userSessions = await db.userSession.findMany();
46+
t.is(userSessions.length, 0);
47+
});

packages/backend/server/src/__tests__/auth/service.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,10 @@ test('should be able to signout multi accounts session', async t => {
192192

193193
const session = await auth.createSession();
194194

195-
await auth.createUserSession(u1.id, session.id);
196-
await auth.createUserSession(u2.id, session.id);
195+
const userSession1 = await auth.createUserSession(u1.id, session.id);
196+
const userSession2 = await auth.createUserSession(u2.id, session.id);
197+
t.not(userSession1.id, userSession2.id);
198+
t.is(userSession1.sessionId, userSession2.sessionId);
197199

198200
await auth.signOut(session.id, u1.id);
199201

packages/backend/server/src/core/auth/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { QuotaModule } from '../quota';
77
import { UserModule } from '../user';
88
import { AuthController } from './controller';
99
import { AuthGuard, AuthWebsocketOptionsProvider } from './guard';
10+
import { AuthCronJob } from './job';
1011
import { AuthResolver } from './resolver';
1112
import { AuthService } from './service';
1213

@@ -16,6 +17,7 @@ import { AuthService } from './service';
1617
AuthService,
1718
AuthResolver,
1819
AuthGuard,
20+
AuthCronJob,
1921
AuthWebsocketOptionsProvider,
2022
],
2123
exports: [AuthService, AuthGuard, AuthWebsocketOptionsProvider],
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { Cron, CronExpression } from '@nestjs/schedule';
3+
4+
import { Models } from '../../models';
5+
6+
@Injectable()
7+
export class AuthCronJob {
8+
constructor(private readonly models: Models) {}
9+
10+
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
11+
async cleanExpiredUserSessions() {
12+
await this.models.session.cleanExpiredUserSessions();
13+
}
14+
}

packages/backend/server/src/core/auth/service.ts

Lines changed: 25 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
2-
import { Cron, CronExpression } from '@nestjs/schedule';
32
import type { User, UserSession } from '@prisma/client';
4-
import { PrismaClient } from '@prisma/client';
53
import type { CookieOptions, Request, Response } from 'express';
64
import { assign, pick } from 'lodash-es';
75

@@ -47,7 +45,6 @@ export class AuthService implements OnApplicationBootstrap {
4745

4846
constructor(
4947
private readonly config: Config,
50-
private readonly db: PrismaClient,
5148
private readonly models: Models,
5249
private readonly mailer: MailService,
5350
private readonly feature: FeatureManagementService,
@@ -105,14 +102,9 @@ export class AuthService implements OnApplicationBootstrap {
105102
async signOut(sessionId: string, userId?: string) {
106103
// sign out all users in the session
107104
if (!userId) {
108-
await this.models.session.delete(sessionId);
105+
await this.models.session.deleteSession(sessionId);
109106
} else {
110-
await this.db.userSession.deleteMany({
111-
where: {
112-
sessionId,
113-
userId,
114-
},
115-
});
107+
await this.models.session.deleteUserSession(userId, sessionId);
116108
}
117109
}
118110

@@ -149,117 +141,50 @@ export class AuthService implements OnApplicationBootstrap {
149141
}
150142

151143
async getUserSessions(sessionId: string) {
152-
return this.db.userSession.findMany({
153-
where: {
154-
sessionId,
155-
OR: [{ expiresAt: { gt: new Date() } }, { expiresAt: null }],
156-
},
157-
orderBy: {
158-
createdAt: 'asc',
159-
},
160-
});
144+
return await this.models.session.findUserSessionsBySessionId(sessionId);
161145
}
162146

163-
async createUserSession(
164-
userId: string,
165-
sessionId?: string,
166-
ttl = this.config.auth.session.ttl
167-
) {
168-
// check whether given session is valid
169-
if (sessionId) {
170-
const session = await this.getSession(sessionId);
171-
172-
if (!session) {
173-
sessionId = undefined;
174-
}
175-
}
176-
177-
if (!sessionId) {
178-
const session = await this.createSession();
179-
sessionId = session.id;
180-
}
181-
182-
const expiresAt = new Date(Date.now() + ttl * 1000);
183-
184-
return this.db.userSession.upsert({
185-
where: {
186-
sessionId_userId: {
187-
sessionId,
188-
userId,
189-
},
190-
},
191-
update: {
192-
expiresAt,
193-
},
194-
create: {
195-
sessionId,
196-
userId,
197-
expiresAt,
198-
},
199-
});
147+
async createUserSession(userId: string, sessionId?: string, ttl?: number) {
148+
return await this.models.session.createOrRefreshUserSession(
149+
userId,
150+
sessionId,
151+
ttl
152+
);
200153
}
201154

202155
async getUserList(sessionId: string) {
203-
const sessions = await this.db.userSession.findMany({
204-
where: {
205-
sessionId,
206-
OR: [
207-
{
208-
expiresAt: null,
209-
},
210-
{
211-
expiresAt: {
212-
gt: new Date(),
213-
},
214-
},
215-
],
216-
},
217-
include: {
156+
const sessions = await this.models.session.findUserSessionsBySessionId(
157+
sessionId,
158+
{
218159
user: true,
219-
},
220-
orderBy: {
221-
createdAt: 'asc',
222-
},
223-
});
224-
160+
}
161+
);
225162
return sessions.map(({ user }) => sessionUser(user));
226163
}
227164

228165
async createSession() {
229-
return await this.models.session.create();
166+
return await this.models.session.createSession();
230167
}
231168

232169
async getSession(sessionId: string) {
233-
return await this.models.session.get(sessionId);
170+
return await this.models.session.getSession(sessionId);
234171
}
235172

236173
async refreshUserSessionIfNeeded(
237174
res: Response,
238-
session: UserSession,
239-
ttr = this.config.auth.session.ttr
175+
userSession: UserSession,
176+
ttr?: number
240177
): Promise<boolean> {
241-
if (
242-
session.expiresAt &&
243-
session.expiresAt.getTime() - Date.now() > ttr * 1000
244-
) {
178+
const newExpiresAt = await this.models.session.refreshUserSessionIfNeeded(
179+
userSession,
180+
ttr
181+
);
182+
if (!newExpiresAt) {
245183
// no need to refresh
246184
return false;
247185
}
248186

249-
const newExpiresAt = new Date(
250-
Date.now() + this.config.auth.session.ttl * 1000
251-
);
252-
253-
await this.db.userSession.update({
254-
where: {
255-
id: session.id,
256-
},
257-
data: {
258-
expiresAt: newExpiresAt,
259-
},
260-
});
261-
262-
res.cookie(AuthService.sessionCookieName, session.sessionId, {
187+
res.cookie(AuthService.sessionCookieName, userSession.sessionId, {
263188
expires: newExpiresAt,
264189
...this.cookieOptions,
265190
});
@@ -268,11 +193,7 @@ export class AuthService implements OnApplicationBootstrap {
268193
}
269194

270195
async revokeUserSessions(userId: string) {
271-
return this.db.userSession.deleteMany({
272-
where: {
273-
userId,
274-
},
275-
});
196+
return await this.models.session.deleteUserSession(userId);
276197
}
277198

278199
getSessionOptionsFromRequest(req: Request) {
@@ -412,15 +333,4 @@ export class AuthService implements OnApplicationBootstrap {
412333
to: email,
413334
});
414335
}
415-
416-
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
417-
async cleanExpiredSessions() {
418-
await this.db.userSession.deleteMany({
419-
where: {
420-
expiresAt: {
421-
lte: new Date(),
422-
},
423-
},
424-
});
425-
}
426336
}

packages/backend/server/src/models/session.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import {
44
PrismaClient,
55
type Session,
66
type User,
7-
type UserSession as _UserSession,
7+
type UserSession,
88
} from '@prisma/client';
99

1010
export type { Session };
1111

1212
import { Config } from '../base';
1313

14-
export type UserSession = _UserSession & { user?: User };
14+
export type { UserSession };
15+
export type UserSessionWithUser = UserSession & { user: User };
1516

1617
@Injectable()
1718
export class SessionModel {
@@ -115,10 +116,12 @@ export class SessionModel {
115116
return newExpiresAt;
116117
}
117118

118-
async findUserSessionsBySessionId(
119+
async findUserSessionsBySessionId<
120+
T extends Prisma.UserSessionInclude | undefined,
121+
>(
119122
sessionId: string,
120-
include?: Prisma.UserSessionInclude
121-
): Promise<UserSession[]> {
123+
include?: T
124+
): Promise<(T extends { user: true } ? UserSessionWithUser : UserSession)[]> {
122125
return await this.db.userSession.findMany({
123126
where: {
124127
sessionId,
@@ -127,7 +130,7 @@ export class SessionModel {
127130
orderBy: {
128131
createdAt: 'asc',
129132
},
130-
include,
133+
include: include as Prisma.UserSessionInclude,
131134
});
132135
}
133136

0 commit comments

Comments
 (0)