Skip to content

Commit 1a1af83

Browse files
committed
test(server): auth tests (#6135)
1 parent 1c9d899 commit 1a1af83

File tree

19 files changed

+1058
-96
lines changed

19 files changed

+1058
-96
lines changed

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

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,15 @@ import {
66
Controller,
77
Get,
88
Header,
9+
HttpStatus,
910
Post,
1011
Query,
1112
Req,
1213
Res,
1314
} from '@nestjs/common';
1415
import type { Request, Response } from 'express';
1516

16-
import {
17-
Config,
18-
PaymentRequiredException,
19-
URLHelper,
20-
} from '../../fundamentals';
17+
import { PaymentRequiredException, URLHelper } from '../../fundamentals';
2118
import { UserService } from '../user';
2219
import { validators } from '../utils/validators';
2320
import { CurrentUser } from './current-user';
@@ -33,7 +30,6 @@ class SignInCredential {
3330
@Controller('/api/auth')
3431
export class AuthController {
3532
constructor(
36-
private readonly config: Config,
3733
private readonly url: URLHelper,
3834
private readonly auth: AuthService,
3935
private readonly user: UserService,
@@ -64,7 +60,7 @@ export class AuthController {
6460
);
6561

6662
await this.auth.setCookie(req, res, user);
67-
res.send(user);
63+
res.status(HttpStatus.OK).send(user);
6864
} else {
6965
// send email magic link
7066
const user = await this.user.findUserByEmail(credential.email);
@@ -77,7 +73,7 @@ export class AuthController {
7773
throw new Error('Failed to send sign-in email.');
7874
}
7975

80-
res.send({
76+
res.status(HttpStatus.OK).send({
8177
email: credential.email,
8278
});
8379
}
@@ -162,22 +158,6 @@ export class AuthController {
162158
return this.url.safeRedirect(res, redirectUri);
163159
}
164160

165-
@Get('/authorize')
166-
async authorize(
167-
@CurrentUser() user: CurrentUser,
168-
@Query('redirect_uri') redirect_uri?: string
169-
) {
170-
const session = await this.auth.createUserSession(
171-
user,
172-
undefined,
173-
this.config.auth.accessToken.ttl
174-
);
175-
176-
this.url.link(redirect_uri ?? '/open-app/redirect', {
177-
token: session.sessionId,
178-
});
179-
}
180-
181161
@Public()
182162
@Get('/session')
183163
async currentSessionUser(@CurrentUser() user?: CurrentUser) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { UserModule } from '../user';
55
import { AuthController } from './controller';
66
import { AuthResolver } from './resolver';
77
import { AuthService } from './service';
8-
import { TokenService } from './token';
8+
import { TokenService, TokenType } from './token';
99

1010
@Module({
1111
imports: [FeatureModule, UserModule],
@@ -17,5 +17,5 @@ export class AuthModule {}
1717

1818
export * from './guard';
1919
export { ClientTokenType } from './resolver';
20-
export { AuthService };
20+
export { AuthService, TokenService, TokenType };
2121
export * from './current-user';

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import type { Request, Response } from 'express';
1818

1919
import { CloudThrottlerGuard, Config, Throttle } from '../../fundamentals';
20+
import { UserService } from '../user';
2021
import { UserType } from '../user/types';
2122
import { validators } from '../utils/validators';
2223
import { CurrentUser } from './current-user';
@@ -48,6 +49,7 @@ export class AuthResolver {
4849
constructor(
4950
private readonly config: Config,
5051
private readonly auth: AuthService,
52+
private readonly user: UserService,
5153
private readonly token: TokenService
5254
) {}
5355

@@ -165,7 +167,7 @@ export class AuthResolver {
165167
throw new ForbiddenException('Invalid token');
166168
}
167169

168-
await this.auth.changePassword(user.email, newPassword);
170+
await this.auth.changePassword(user.id, newPassword);
169171

170172
return user;
171173
}
@@ -319,7 +321,7 @@ export class AuthResolver {
319321
throw new ForbiddenException('Invalid token');
320322
}
321323

322-
const hasRegistered = await this.auth.getUserByEmail(email);
324+
const hasRegistered = await this.user.findUserByEmail(email);
323325

324326
if (hasRegistered) {
325327
if (hasRegistered.id !== user.id) {

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

Lines changed: 30 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,39 @@ import {
22
BadRequestException,
33
Injectable,
44
NotAcceptableException,
5-
NotFoundException,
65
OnApplicationBootstrap,
76
} from '@nestjs/common';
87
import type { User } from '@prisma/client';
98
import { PrismaClient } from '@prisma/client';
109
import type { CookieOptions, Request, Response } from 'express';
1110
import { assign, omit } from 'lodash-es';
1211

13-
import {
14-
Config,
15-
CryptoHelper,
16-
MailService,
17-
SessionCache,
18-
} from '../../fundamentals';
12+
import { Config, CryptoHelper, MailService } from '../../fundamentals';
1913
import { FeatureManagementService } from '../features/management';
2014
import { UserService } from '../user/service';
2115
import type { CurrentUser } from './current-user';
2216

2317
export function parseAuthUserSeqNum(value: any) {
18+
let seq: number = 0;
2419
switch (typeof value) {
2520
case 'number': {
26-
return value;
21+
seq = value;
22+
break;
2723
}
2824
case 'string': {
29-
value = Number.parseInt(value);
30-
return Number.isNaN(value) ? 0 : value;
25+
const result = value.match(/^([\d{0, 10}])$/);
26+
if (result?.[1]) {
27+
seq = Number(result[1]);
28+
}
29+
break;
3130
}
3231

3332
default: {
34-
return 0;
33+
seq = 0;
3534
}
3635
}
36+
37+
return Math.max(0, seq);
3738
}
3839

3940
export function sessionUser(
@@ -57,7 +58,6 @@ export class AuthService implements OnApplicationBootstrap {
5758
sameSite: 'lax',
5859
httpOnly: true,
5960
path: '/',
60-
domain: this.config.host,
6161
secure: this.config.https,
6262
};
6363
static readonly sessionCookieName = 'sid';
@@ -69,8 +69,7 @@ export class AuthService implements OnApplicationBootstrap {
6969
private readonly mailer: MailService,
7070
private readonly feature: FeatureManagementService,
7171
private readonly user: UserService,
72-
private readonly crypto: CryptoHelper,
73-
private readonly cache: SessionCache
72+
private readonly crypto: CryptoHelper
7473
) {}
7574

7675
async onApplicationBootstrap() {
@@ -90,7 +89,7 @@ export class AuthService implements OnApplicationBootstrap {
9089
email: string,
9190
password: string
9291
): Promise<CurrentUser> {
93-
const user = await this.getUserByEmail(email);
92+
const user = await this.user.findUserByEmail(email);
9493

9594
if (user) {
9695
throw new BadRequestException('Email was taken');
@@ -111,12 +110,12 @@ export class AuthService implements OnApplicationBootstrap {
111110
const user = await this.user.findUserWithHashedPasswordByEmail(email);
112111

113112
if (!user) {
114-
throw new NotFoundException('User Not Found');
113+
throw new NotAcceptableException('Invalid sign in credentials');
115114
}
116115

117116
if (!user.password) {
118117
throw new NotAcceptableException(
119-
'User Password is not set. Should login throw email link.'
118+
'User Password is not set. Should login through email link.'
120119
);
121120
}
122121

@@ -126,28 +125,12 @@ export class AuthService implements OnApplicationBootstrap {
126125
);
127126

128127
if (!passwordMatches) {
129-
throw new NotAcceptableException('Incorrect Password');
128+
throw new NotAcceptableException('Invalid sign in credentials');
130129
}
131130

132131
return sessionUser(user);
133132
}
134133

135-
async getUserWithCache(token: string, seq = 0) {
136-
const cacheKey = `session:${token}:${seq}`;
137-
let user = await this.cache.get<CurrentUser | null>(cacheKey);
138-
if (user) {
139-
return user;
140-
}
141-
142-
user = await this.getUser(token, seq);
143-
144-
if (user) {
145-
await this.cache.set(cacheKey, user);
146-
}
147-
148-
return user;
149-
}
150-
151134
async getUser(token: string, seq = 0): Promise<CurrentUser | null> {
152135
const session = await this.getSession(token);
153136

@@ -198,7 +181,16 @@ export class AuthService implements OnApplicationBootstrap {
198181
// Session
199182
// | { user: LimitedUser { email, avatarUrl }, expired: true }
200183
// | { user: User, expired: false }
201-
return users.map(sessionUser);
184+
return session.userSessions
185+
.map(userSession => {
186+
// keep users in the same order as userSessions
187+
const user = users.find(({ id }) => id === userSession.userId);
188+
if (!user) {
189+
return null;
190+
}
191+
return sessionUser(user);
192+
})
193+
.filter(Boolean) as CurrentUser[];
202194
}
203195

204196
async signOut(token: string, seq = 0) {
@@ -319,12 +311,8 @@ export class AuthService implements OnApplicationBootstrap {
319311
});
320312
}
321313

322-
async getUserByEmail(email: string) {
323-
return this.user.findUserByEmail(email);
324-
}
325-
326-
async changePassword(email: string, newPassword: string): Promise<User> {
327-
const user = await this.getUserByEmail(email);
314+
async changePassword(id: string, newPassword: string): Promise<User> {
315+
const user = await this.user.findUserById(id);
328316

329317
if (!user) {
330318
throw new BadRequestException('Invalid email');
@@ -343,11 +331,7 @@ export class AuthService implements OnApplicationBootstrap {
343331
}
344332

345333
async changeEmail(id: string, newEmail: string): Promise<User> {
346-
const user = await this.db.user.findUnique({
347-
where: {
348-
id,
349-
},
350-
});
334+
const user = await this.user.findUserById(id);
351335

352336
if (!user) {
353337
throw new BadRequestException('Invalid email');

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { randomUUID } from 'node:crypto';
22

33
import { Injectable } from '@nestjs/common';
4+
import { Cron, CronExpression } from '@nestjs/schedule';
45
import { PrismaClient } from '@prisma/client';
56

67
import { CryptoHelper } from '../../fundamentals/helpers';
@@ -81,4 +82,15 @@ export class TokenService {
8182

8283
return valid ? record : null;
8384
}
85+
86+
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
87+
cleanExpiredTokens() {
88+
return this.db.verificationToken.deleteMany({
89+
where: {
90+
expiresAt: {
91+
lte: new Date(),
92+
},
93+
},
94+
});
95+
}
8496
}

packages/backend/server/src/fundamentals/config/def.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,6 @@ declare global {
1313
}
1414
}
1515

16-
export enum ExternalAccount {
17-
github = 'github',
18-
google = 'google',
19-
firebase = 'firebase',
20-
}
21-
2216
export type ServerFlavor = 'allinone' | 'graphql' | 'sync';
2317
export type AFFINE_ENV = 'dev' | 'beta' | 'production';
2418
export type NODE_ENV = 'development' | 'test' | 'production';

packages/backend/server/src/fundamentals/prisma/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import { PrismaService } from './service';
66
// only `PrismaClient` can be injected
77
const clientProvider: Provider = {
88
provide: PrismaClient,
9-
useClass: PrismaService,
9+
useFactory: () => {
10+
if (PrismaService.INSTANCE) {
11+
return PrismaService.INSTANCE;
12+
}
13+
14+
return new PrismaService();
15+
},
1016
};
1117

1218
@Global()

packages/backend/server/src/fundamentals/prisma/service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export class PrismaService
1919
}
2020

2121
async onModuleDestroy(): Promise<void> {
22-
await this.$disconnect();
22+
if (!AFFiNE.node.test) {
23+
await this.$disconnect();
24+
PrismaService.INSTANCE = null;
25+
}
2326
}
2427
}

packages/backend/server/src/plugins/oauth/controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class OAuthController {
4040
const provider = this.providerFactory.get(providerName);
4141

4242
if (!provider) {
43-
throw new BadRequestException('Invalid provider');
43+
throw new BadRequestException('Invalid OAuth provider');
4444
}
4545

4646
const state = await this.oauth.saveOAuthState({

packages/backend/server/src/plugins/oauth/register.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function registerOAuthProvider(
1616
@Injectable()
1717
export class OAuthProviderFactory {
1818
get providers() {
19-
return PROVIDERS.keys();
19+
return Array.from(PROVIDERS.keys());
2020
}
2121

2222
get(name: OAuthProviderName): OAuthProvider | undefined {

0 commit comments

Comments
 (0)