refactor(server): use verificationToken model instead of tokenService (#9657)

This commit is contained in:
fengmk2
2025-01-14 03:39:05 +00:00
parent 290b2074c8
commit ee99b0cc9d
6 changed files with 55 additions and 271 deletions

View File

@@ -26,12 +26,12 @@ import {
URLHelper,
UseNamedGuard,
} from '../../base';
import { Models, TokenType } from '../../models';
import { UserService } from '../user';
import { validators } from '../utils/validators';
import { Public } from './guard';
import { AuthService } from './service';
import { CurrentUser, Session } from './session';
import { TokenService, TokenType } from './token';
interface PreflightResponse {
registered: boolean;
@@ -57,7 +57,7 @@ export class AuthController {
private readonly url: URLHelper,
private readonly auth: AuthService,
private readonly user: UserService,
private readonly token: TokenService,
private readonly models: Models,
private readonly config: Config,
private readonly runtime: Runtime
) {
@@ -194,7 +194,10 @@ export class AuthController {
}
}
const token = await this.token.createToken(TokenType.SignIn, email);
const token = await this.models.verificationToken.create(
TokenType.SignIn,
email
);
const magicLink = this.url.link(callbackUrl, {
token,
@@ -248,9 +251,13 @@ export class AuthController {
validators.assertValidEmail(email);
const tokenRecord = await this.token.verifyToken(TokenType.SignIn, token, {
credential: email,
});
const tokenRecord = await this.models.verificationToken.verify(
TokenType.SignIn,
token,
{
credential: email,
}
);
if (!tokenRecord) {
throw new InvalidEmailToken();

View File

@@ -9,23 +9,21 @@ import { AuthController } from './controller';
import { AuthGuard, AuthWebsocketOptionsProvider } from './guard';
import { AuthResolver } from './resolver';
import { AuthService } from './service';
import { TokenService, TokenType } from './token';
@Module({
imports: [FeatureModule, UserModule, QuotaModule],
providers: [
AuthService,
AuthResolver,
TokenService,
AuthGuard,
AuthWebsocketOptionsProvider,
],
exports: [AuthService, AuthGuard, AuthWebsocketOptionsProvider, TokenService],
exports: [AuthService, AuthGuard, AuthWebsocketOptionsProvider],
controllers: [AuthController],
})
export class AuthModule {}
export * from './guard';
export { ClientTokenType } from './resolver';
export { AuthService, TokenService, TokenType };
export { AuthService };
export * from './session';

View File

@@ -21,6 +21,7 @@ import {
Throttle,
URLHelper,
} from '../../base';
import { Models, TokenType } from '../../models';
import { Admin } from '../common';
import { UserService } from '../user';
import { UserType } from '../user/types';
@@ -28,7 +29,6 @@ import { validators } from '../utils/validators';
import { Public } from './guard';
import { AuthService } from './service';
import { CurrentUser } from './session';
import { TokenService, TokenType } from './token';
@ObjectType('tokenType')
export class ClientTokenType {
@@ -49,7 +49,7 @@ export class AuthResolver {
private readonly url: URLHelper,
private readonly auth: AuthService,
private readonly user: UserService,
private readonly token: TokenService
private readonly models: Models
) {}
@SkipThrottle()
@@ -96,7 +96,7 @@ export class AuthResolver {
}
// NOTE: Set & Change password are using the same token type.
const valid = await this.token.verifyToken(
const valid = await this.models.verificationToken.verify(
TokenType.ChangePassword,
token,
{
@@ -121,9 +121,13 @@ export class AuthResolver {
@Args('email') email: string
) {
// @see [sendChangeEmail]
const valid = await this.token.verifyToken(TokenType.VerifyEmail, token, {
credential: user.id,
});
const valid = await this.models.verificationToken.verify(
TokenType.VerifyEmail,
token,
{
credential: user.id,
}
);
if (!valid) {
throw new InvalidEmailToken();
@@ -152,7 +156,7 @@ export class AuthResolver {
throw new EmailVerificationRequired();
}
const token = await this.token.createToken(
const token = await this.models.verificationToken.create(
TokenType.ChangePassword,
user.id
);
@@ -195,7 +199,10 @@ export class AuthResolver {
throw new EmailVerificationRequired();
}
const token = await this.token.createToken(TokenType.ChangeEmail, user.id);
const token = await this.models.verificationToken.create(
TokenType.ChangeEmail,
user.id
);
const url = this.url.link(callbackUrl, { token });
@@ -215,9 +222,13 @@ export class AuthResolver {
}
validators.assertValidEmail(email);
const valid = await this.token.verifyToken(TokenType.ChangeEmail, token, {
credential: user.id,
});
const valid = await this.models.verificationToken.verify(
TokenType.ChangeEmail,
token,
{
credential: user.id,
}
);
if (!valid) {
throw new InvalidEmailToken();
@@ -233,7 +244,7 @@ export class AuthResolver {
}
}
const verifyEmailToken = await this.token.createToken(
const verifyEmailToken = await this.models.verificationToken.create(
TokenType.VerifyEmail,
user.id
);
@@ -249,7 +260,10 @@ export class AuthResolver {
@CurrentUser() user: CurrentUser,
@Args('callbackUrl') callbackUrl: string
) {
const token = await this.token.createToken(TokenType.VerifyEmail, user.id);
const token = await this.models.verificationToken.create(
TokenType.VerifyEmail,
user.id
);
const url = this.url.link(callbackUrl, { token });
@@ -266,9 +280,13 @@ export class AuthResolver {
throw new EmailTokenNotFound();
}
const valid = await this.token.verifyToken(TokenType.VerifyEmail, token, {
credential: user.id,
});
const valid = await this.models.verificationToken.verify(
TokenType.VerifyEmail,
token,
{
credential: user.id,
}
);
if (!valid) {
throw new InvalidEmailToken();
@@ -287,7 +305,7 @@ export class AuthResolver {
@Args('userId') userId: string,
@Args('callbackUrl') callbackUrl: string
): Promise<string> {
const token = await this.token.createToken(
const token = await this.models.verificationToken.create(
TokenType.ChangePassword,
userId
);

View File

@@ -1,146 +0,0 @@
import { randomUUID } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaClient } from '@prisma/client';
import { CryptoHelper } from '../../base/helpers';
export enum TokenType {
SignIn,
VerifyEmail,
ChangeEmail,
ChangePassword,
Challenge,
}
@Injectable()
export class TokenService {
constructor(
private readonly db: PrismaClient,
private readonly crypto: CryptoHelper
) {}
async createToken(
type: TokenType,
credential?: string,
ttlInSec: number = 30 * 60
) {
const plaintextToken = randomUUID();
const { token } = await this.db.verificationToken.create({
data: {
type,
token: plaintextToken,
credential,
expiresAt: new Date(Date.now() + ttlInSec * 1000),
},
});
return this.crypto.encrypt(token);
}
/**
* get token by type
*
* token will be revoked if expired or keep is not set
*/
async getToken(type: TokenType, token: string, keep?: boolean) {
token = this.crypto.decrypt(token);
const record = await this.db.verificationToken.findUnique({
where: {
type_token: {
token,
type,
},
},
});
if (!record) {
return null;
}
const expired = record.expiresAt <= new Date();
// always revoke expired token
if (expired || !keep) {
const deleted = await this.revokeToken(type, token);
// already deleted, means token has been used
if (!deleted.count) {
return null;
}
}
return !expired ? record : null;
}
/**
* get token and verify credential
*
* if credential is not provided, it will be failed
*
* token will be revoked if expired or keep is not set
*/
async verifyToken(
type: TokenType,
token: string,
{
credential,
keep,
}: {
credential?: string;
keep?: boolean;
} = {}
) {
token = this.crypto.decrypt(token);
const record = await this.db.verificationToken.findUnique({
where: {
type_token: {
token,
type,
},
},
});
if (!record) {
return null;
}
const expired = record.expiresAt <= new Date();
const valid =
!expired && (!record.credential || record.credential === credential);
// always revoke expired token
if (expired || (valid && !keep)) {
const deleted = await this.revokeToken(type, token);
// already deleted, means token has been used
if (!deleted.count) {
return null;
}
}
return valid ? record : null;
}
async revokeToken(type: TokenType, token: string) {
return await this.db.verificationToken.deleteMany({
where: {
token,
type,
},
});
}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async cleanExpiredTokens() {
await this.db.verificationToken.deleteMany({
where: {
expiresAt: {
lte: new Date(),
},
},
});
}
}