test(server): auth tests (#6135)

This commit is contained in:
forehalo
2024-03-26 02:24:17 +00:00
parent 1c9d899831
commit 1a1af83375
19 changed files with 1058 additions and 96 deletions

View File

@@ -6,6 +6,7 @@ import {
Controller,
Get,
Header,
HttpStatus,
Post,
Query,
Req,
@@ -13,11 +14,7 @@ import {
} from '@nestjs/common';
import type { Request, Response } from 'express';
import {
Config,
PaymentRequiredException,
URLHelper,
} from '../../fundamentals';
import { PaymentRequiredException, URLHelper } from '../../fundamentals';
import { UserService } from '../user';
import { validators } from '../utils/validators';
import { CurrentUser } from './current-user';
@@ -33,7 +30,6 @@ class SignInCredential {
@Controller('/api/auth')
export class AuthController {
constructor(
private readonly config: Config,
private readonly url: URLHelper,
private readonly auth: AuthService,
private readonly user: UserService,
@@ -64,7 +60,7 @@ export class AuthController {
);
await this.auth.setCookie(req, res, user);
res.send(user);
res.status(HttpStatus.OK).send(user);
} else {
// send email magic link
const user = await this.user.findUserByEmail(credential.email);
@@ -77,7 +73,7 @@ export class AuthController {
throw new Error('Failed to send sign-in email.');
}
res.send({
res.status(HttpStatus.OK).send({
email: credential.email,
});
}
@@ -162,22 +158,6 @@ export class AuthController {
return this.url.safeRedirect(res, redirectUri);
}
@Get('/authorize')
async authorize(
@CurrentUser() user: CurrentUser,
@Query('redirect_uri') redirect_uri?: string
) {
const session = await this.auth.createUserSession(
user,
undefined,
this.config.auth.accessToken.ttl
);
this.url.link(redirect_uri ?? '/open-app/redirect', {
token: session.sessionId,
});
}
@Public()
@Get('/session')
async currentSessionUser(@CurrentUser() user?: CurrentUser) {

View File

@@ -5,7 +5,7 @@ import { UserModule } from '../user';
import { AuthController } from './controller';
import { AuthResolver } from './resolver';
import { AuthService } from './service';
import { TokenService } from './token';
import { TokenService, TokenType } from './token';
@Module({
imports: [FeatureModule, UserModule],
@@ -17,5 +17,5 @@ export class AuthModule {}
export * from './guard';
export { ClientTokenType } from './resolver';
export { AuthService };
export { AuthService, TokenService, TokenType };
export * from './current-user';

View File

@@ -17,6 +17,7 @@ import {
import type { Request, Response } from 'express';
import { CloudThrottlerGuard, Config, Throttle } from '../../fundamentals';
import { UserService } from '../user';
import { UserType } from '../user/types';
import { validators } from '../utils/validators';
import { CurrentUser } from './current-user';
@@ -48,6 +49,7 @@ export class AuthResolver {
constructor(
private readonly config: Config,
private readonly auth: AuthService,
private readonly user: UserService,
private readonly token: TokenService
) {}
@@ -165,7 +167,7 @@ export class AuthResolver {
throw new ForbiddenException('Invalid token');
}
await this.auth.changePassword(user.email, newPassword);
await this.auth.changePassword(user.id, newPassword);
return user;
}
@@ -319,7 +321,7 @@ export class AuthResolver {
throw new ForbiddenException('Invalid token');
}
const hasRegistered = await this.auth.getUserByEmail(email);
const hasRegistered = await this.user.findUserByEmail(email);
if (hasRegistered) {
if (hasRegistered.id !== user.id) {

View File

@@ -2,7 +2,6 @@ import {
BadRequestException,
Injectable,
NotAcceptableException,
NotFoundException,
OnApplicationBootstrap,
} from '@nestjs/common';
import type { User } from '@prisma/client';
@@ -10,30 +9,32 @@ import { PrismaClient } from '@prisma/client';
import type { CookieOptions, Request, Response } from 'express';
import { assign, omit } from 'lodash-es';
import {
Config,
CryptoHelper,
MailService,
SessionCache,
} from '../../fundamentals';
import { Config, CryptoHelper, MailService } from '../../fundamentals';
import { FeatureManagementService } from '../features/management';
import { UserService } from '../user/service';
import type { CurrentUser } from './current-user';
export function parseAuthUserSeqNum(value: any) {
let seq: number = 0;
switch (typeof value) {
case 'number': {
return value;
seq = value;
break;
}
case 'string': {
value = Number.parseInt(value);
return Number.isNaN(value) ? 0 : value;
const result = value.match(/^([\d{0, 10}])$/);
if (result?.[1]) {
seq = Number(result[1]);
}
break;
}
default: {
return 0;
seq = 0;
}
}
return Math.max(0, seq);
}
export function sessionUser(
@@ -57,7 +58,6 @@ export class AuthService implements OnApplicationBootstrap {
sameSite: 'lax',
httpOnly: true,
path: '/',
domain: this.config.host,
secure: this.config.https,
};
static readonly sessionCookieName = 'sid';
@@ -69,8 +69,7 @@ export class AuthService implements OnApplicationBootstrap {
private readonly mailer: MailService,
private readonly feature: FeatureManagementService,
private readonly user: UserService,
private readonly crypto: CryptoHelper,
private readonly cache: SessionCache
private readonly crypto: CryptoHelper
) {}
async onApplicationBootstrap() {
@@ -90,7 +89,7 @@ export class AuthService implements OnApplicationBootstrap {
email: string,
password: string
): Promise<CurrentUser> {
const user = await this.getUserByEmail(email);
const user = await this.user.findUserByEmail(email);
if (user) {
throw new BadRequestException('Email was taken');
@@ -111,12 +110,12 @@ export class AuthService implements OnApplicationBootstrap {
const user = await this.user.findUserWithHashedPasswordByEmail(email);
if (!user) {
throw new NotFoundException('User Not Found');
throw new NotAcceptableException('Invalid sign in credentials');
}
if (!user.password) {
throw new NotAcceptableException(
'User Password is not set. Should login throw email link.'
'User Password is not set. Should login through email link.'
);
}
@@ -126,28 +125,12 @@ export class AuthService implements OnApplicationBootstrap {
);
if (!passwordMatches) {
throw new NotAcceptableException('Incorrect Password');
throw new NotAcceptableException('Invalid sign in credentials');
}
return sessionUser(user);
}
async getUserWithCache(token: string, seq = 0) {
const cacheKey = `session:${token}:${seq}`;
let user = await this.cache.get<CurrentUser | null>(cacheKey);
if (user) {
return user;
}
user = await this.getUser(token, seq);
if (user) {
await this.cache.set(cacheKey, user);
}
return user;
}
async getUser(token: string, seq = 0): Promise<CurrentUser | null> {
const session = await this.getSession(token);
@@ -198,7 +181,16 @@ export class AuthService implements OnApplicationBootstrap {
// Session
// | { user: LimitedUser { email, avatarUrl }, expired: true }
// | { user: User, expired: false }
return users.map(sessionUser);
return session.userSessions
.map(userSession => {
// keep users in the same order as userSessions
const user = users.find(({ id }) => id === userSession.userId);
if (!user) {
return null;
}
return sessionUser(user);
})
.filter(Boolean) as CurrentUser[];
}
async signOut(token: string, seq = 0) {
@@ -319,12 +311,8 @@ export class AuthService implements OnApplicationBootstrap {
});
}
async getUserByEmail(email: string) {
return this.user.findUserByEmail(email);
}
async changePassword(email: string, newPassword: string): Promise<User> {
const user = await this.getUserByEmail(email);
async changePassword(id: string, newPassword: string): Promise<User> {
const user = await this.user.findUserById(id);
if (!user) {
throw new BadRequestException('Invalid email');
@@ -343,11 +331,7 @@ export class AuthService implements OnApplicationBootstrap {
}
async changeEmail(id: string, newEmail: string): Promise<User> {
const user = await this.db.user.findUnique({
where: {
id,
},
});
const user = await this.user.findUserById(id);
if (!user) {
throw new BadRequestException('Invalid email');

View File

@@ -1,6 +1,7 @@
import { randomUUID } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaClient } from '@prisma/client';
import { CryptoHelper } from '../../fundamentals/helpers';
@@ -81,4 +82,15 @@ export class TokenService {
return valid ? record : null;
}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
cleanExpiredTokens() {
return this.db.verificationToken.deleteMany({
where: {
expiresAt: {
lte: new Date(),
},
},
});
}
}

View File

@@ -13,12 +13,6 @@ declare global {
}
}
export enum ExternalAccount {
github = 'github',
google = 'google',
firebase = 'firebase',
}
export type ServerFlavor = 'allinone' | 'graphql' | 'sync';
export type AFFINE_ENV = 'dev' | 'beta' | 'production';
export type NODE_ENV = 'development' | 'test' | 'production';

View File

@@ -6,7 +6,13 @@ import { PrismaService } from './service';
// only `PrismaClient` can be injected
const clientProvider: Provider = {
provide: PrismaClient,
useClass: PrismaService,
useFactory: () => {
if (PrismaService.INSTANCE) {
return PrismaService.INSTANCE;
}
return new PrismaService();
},
};
@Global()

View File

@@ -19,6 +19,9 @@ export class PrismaService
}
async onModuleDestroy(): Promise<void> {
await this.$disconnect();
if (!AFFiNE.node.test) {
await this.$disconnect();
PrismaService.INSTANCE = null;
}
}
}

View File

@@ -40,7 +40,7 @@ export class OAuthController {
const provider = this.providerFactory.get(providerName);
if (!provider) {
throw new BadRequestException('Invalid provider');
throw new BadRequestException('Invalid OAuth provider');
}
const state = await this.oauth.saveOAuthState({

View File

@@ -16,7 +16,7 @@ export function registerOAuthProvider(
@Injectable()
export class OAuthProviderFactory {
get providers() {
return PROVIDERS.keys();
return Array.from(PROVIDERS.keys());
}
get(name: OAuthProviderName): OAuthProvider | undefined {