mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +00:00
144 lines
3.1 KiB
TypeScript
144 lines
3.1 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
|
|
import { Injectable } from '@nestjs/common';
|
|
import { type VerificationToken } from '@prisma/client';
|
|
|
|
import { CryptoHelper } from '../base/helpers';
|
|
import { BaseModel } from './base';
|
|
|
|
export type { VerificationToken };
|
|
|
|
export enum TokenType {
|
|
SignIn,
|
|
VerifyEmail,
|
|
ChangeEmail,
|
|
ChangePassword,
|
|
Challenge,
|
|
}
|
|
|
|
@Injectable()
|
|
export class VerificationTokenModel extends BaseModel {
|
|
constructor(private readonly crypto: CryptoHelper) {
|
|
super();
|
|
}
|
|
|
|
/**
|
|
* create token by type and credential (optional) with ttl in seconds (default 30 minutes)
|
|
*/
|
|
async create(
|
|
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 deleted if expired or keep is not set
|
|
*/
|
|
async get(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 isExpired = record.expiresAt <= new Date();
|
|
|
|
// always delete expired token
|
|
// or if keep is not set for one time token
|
|
if (isExpired || !keep) {
|
|
const count = await this.delete(type, token);
|
|
|
|
// already deleted, means token has been used
|
|
if (!count) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return !isExpired ? record : null;
|
|
}
|
|
|
|
/**
|
|
* get token and verify credential
|
|
*
|
|
* if credential is not provided, it will be failed
|
|
*
|
|
* token will be deleted if expired or keep is not set
|
|
*/
|
|
async verify(
|
|
type: TokenType,
|
|
token: string,
|
|
{
|
|
credential,
|
|
keep,
|
|
}: {
|
|
credential?: string;
|
|
keep?: boolean;
|
|
} = {}
|
|
) {
|
|
const record = await this.get(type, token, true);
|
|
if (!record) {
|
|
return null;
|
|
}
|
|
|
|
const valid = !record.credential || record.credential === credential;
|
|
// keep is not set for one time valid token
|
|
if (valid && !keep) {
|
|
const count = await this.delete(type, record.token);
|
|
|
|
// already deleted, means token has been used
|
|
if (!count) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return valid ? record : null;
|
|
}
|
|
|
|
async delete(type: TokenType, token: string) {
|
|
const { count } = await this.db.verificationToken.deleteMany({
|
|
where: {
|
|
token,
|
|
type,
|
|
},
|
|
});
|
|
this.logger.log(`Deleted token success by type ${type} and token ${token}`);
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* clean expired tokens
|
|
*/
|
|
async cleanExpired() {
|
|
const { count } = await this.db.verificationToken.deleteMany({
|
|
where: {
|
|
expiresAt: {
|
|
lte: new Date(),
|
|
},
|
|
},
|
|
});
|
|
this.logger.log(`Cleaned ${count} expired tokens`);
|
|
return count;
|
|
}
|
|
}
|