Skip to content

Commit 812b46c

Browse files
committed
refactor(server): use userSession model in auth service
1 parent 1ab71b0 commit 812b46c

File tree

8 files changed

+99
-123
lines changed

8 files changed

+99
-123
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('[email protected]', '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('[email protected]', '1');
29+
const user2 = await auth.signUp('[email protected]', '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: 26 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
2-
import { Cron, CronExpression } from '@nestjs/schedule';
3-
import type { User, UserSession } from '@prisma/client';
4-
import { PrismaClient } from '@prisma/client';
52
import type { CookieOptions, Request, Response } from 'express';
63
import { assign, pick } from 'lodash-es';
74

85
import { Config, MailService, SignUpForbidden } from '../../base';
9-
import { Models } from '../../models';
6+
import { Models, type User, type UserSession } from '../../models';
107
import { FeatureManagementService } from '../features/management';
118
import { QuotaService } from '../quota/service';
129
import { QuotaType } from '../quota/types';
@@ -47,7 +44,6 @@ export class AuthService implements OnApplicationBootstrap {
4744

4845
constructor(
4946
private readonly config: Config,
50-
private readonly db: PrismaClient,
5147
private readonly models: Models,
5248
private readonly mailer: MailService,
5349
private readonly feature: FeatureManagementService,
@@ -105,14 +101,9 @@ export class AuthService implements OnApplicationBootstrap {
105101
async signOut(sessionId: string, userId?: string) {
106102
// sign out all users in the session
107103
if (!userId) {
108-
await this.models.session.delete(sessionId);
104+
await this.models.session.deleteSession(sessionId);
109105
} else {
110-
await this.db.userSession.deleteMany({
111-
where: {
112-
sessionId,
113-
userId,
114-
},
115-
});
106+
await this.models.session.deleteUserSession(userId, sessionId);
116107
}
117108
}
118109

@@ -149,117 +140,50 @@ export class AuthService implements OnApplicationBootstrap {
149140
}
150141

151142
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-
});
143+
return await this.models.session.findUserSessionsBySessionId(sessionId);
161144
}
162145

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-
});
146+
async createUserSession(userId: string, sessionId?: string, ttl?: number) {
147+
return await this.models.session.createOrRefreshUserSession(
148+
userId,
149+
sessionId,
150+
ttl
151+
);
200152
}
201153

202154
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: {
155+
const sessions = await this.models.session.findUserSessionsBySessionId(
156+
sessionId,
157+
{
218158
user: true,
219-
},
220-
orderBy: {
221-
createdAt: 'asc',
222-
},
223-
});
224-
159+
}
160+
);
225161
return sessions.map(({ user }) => sessionUser(user));
226162
}
227163

228164
async createSession() {
229-
return await this.models.session.create();
165+
return await this.models.session.createSession();
230166
}
231167

232168
async getSession(sessionId: string) {
233-
return await this.models.session.get(sessionId);
169+
return await this.models.session.getSession(sessionId);
234170
}
235171

236172
async refreshUserSessionIfNeeded(
237173
res: Response,
238-
session: UserSession,
239-
ttr = this.config.auth.session.ttr
174+
userSession: UserSession,
175+
ttr?: number
240176
): Promise<boolean> {
241-
if (
242-
session.expiresAt &&
243-
session.expiresAt.getTime() - Date.now() > ttr * 1000
244-
) {
177+
const newExpiresAt = await this.models.session.refreshUserSessionIfNeeded(
178+
userSession,
179+
ttr
180+
);
181+
if (!newExpiresAt) {
245182
// no need to refresh
246183
return false;
247184
}
248185

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, {
186+
res.cookie(AuthService.sessionCookieName, userSession.sessionId, {
263187
expires: newExpiresAt,
264188
...this.cookieOptions,
265189
});
@@ -268,11 +192,7 @@ export class AuthService implements OnApplicationBootstrap {
268192
}
269193

270194
async revokeUserSessions(userId: string) {
271-
return this.db.userSession.deleteMany({
272-
where: {
273-
userId,
274-
},
275-
});
195+
return await this.models.session.deleteUserSession(userId);
276196
}
277197

278198
getSessionOptionsFromRequest(req: Request) {
@@ -412,15 +332,4 @@ export class AuthService implements OnApplicationBootstrap {
412332
to: email,
413333
});
414334
}
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-
}
426335
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { ExecutionContext } from '@nestjs/common';
22
import { createParamDecorator } from '@nestjs/common';
3-
import { User, UserSession } from '@prisma/client';
43

54
import { getRequestResponseFromContext } from '../../base';
5+
import type { User, UserSession } from '../../models';
66

77
/**
88
* Used to fetch current user from the request context.
@@ -37,7 +37,7 @@ import { getRequestResponseFromContext } from '../../base';
3737
* ```
3838
*/
3939
// interface and variable don't conflict
40-
// eslint-disable-next-line no-redeclare
40+
// oxlint-disable-next-line no-redeclare
4141
export const CurrentUser = createParamDecorator(
4242
(_: unknown, context: ExecutionContext) => {
4343
return getRequestResponseFromContext(context).req.session?.user;
@@ -51,7 +51,7 @@ export interface CurrentUser
5151
}
5252

5353
// interface and variable don't conflict
54-
// eslint-disable-next-line no-redeclare
54+
// oxlint-disable-next-line no-redeclare
5555
export const Session = createParamDecorator(
5656
(_: unknown, context: ExecutionContext) => {
5757
return getRequestResponseFromContext(context).req.session;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { SessionModel } from './session';
44
import { UserModel } from './user';
55
import { VerificationTokenModel } from './verification-token';
66

7+
export * from './session';
8+
export * from './user';
79
export * from './verification-token';
810

911
const models = [UserModel, SessionModel, VerificationTokenModel] as const;

0 commit comments

Comments
 (0)