mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
test(server): auth tests (#6135)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -19,6 +19,9 @@ export class PrismaService
|
||||
}
|
||||
|
||||
async onModuleDestroy(): Promise<void> {
|
||||
await this.$disconnect();
|
||||
if (!AFFiNE.node.test) {
|
||||
await this.$disconnect();
|
||||
PrismaService.INSTANCE = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user