import { JwtPayload } from 'jsonwebtoken'; import { InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose'; import { Strategy } from './strategy'; import { STRATEGIES } from '../../constants/strategies'; import { TokenProps, verify as verifyJwt } from '../../utils/jwt'; import { encrypt, verify as verifyPassword } from '../../utils/password'; import { generateLoginToken, generateResetToken } from '../../utils/tokens'; import { getPasswordResetPath } from '../../utils/links'; import { Status } from '../../constants/auth'; export type Auth = { is2FA?: boolean; record: StringSchemaDefinition; username: string; }; export type AuthPrivate = Auth & { status: Status; strategies: Types.ArraySubdocument; }; export interface AuthMethods { authenticate(password: string): boolean; getAuthStrategy(method?: STRATEGIES): Strategy | false; getResetLink(route: string): Promise; getResetToken(): Promise; getToken(props?: Omit | void): string; isActive(): boolean; setPassword(password: string): Promise; } export interface AuthModel extends Model { authenticate(username: string, password?: string): string | false; findByUsername(username: string): Promise; isUsernameAvailable(username: string): Promise; findUserForReset(strategy: STRATEGIES, token: string): Promise; resetPassword(token: string, password: string): Promise; } export const AuthSchema = new Schema( { is2FA: { type: Boolean, default: false }, record: { type: Types.ObjectId }, strategies: { type: Array, required: true }, status: { type: Number, enum: Object.values(Status), default: Status.UNVERIFIED }, username: { type: String, required: true, unique: true }, }, { minimize: true, timestamps: true, }, ); AuthSchema.methods = { authenticate(password: string) { const strategy = this.getAuthStrategy(STRATEGIES.LOCAL); return !!strategy && verifyPassword(password, strategy.key); }, getAuthStrategy(method = STRATEGIES.LOCAL) { return this.strategies.filter((strategy: Strategy) => strategy.method === method).pop() || false; }, getToken(props = {}) { return generateLoginToken(this._id, this.status); }, async getResetLink(route) { const token = await this.getResetToken(); if (token) { const resetUrl = getPasswordResetPath(token); console.log('[sendPasswordReset] resetUrl:', resetUrl); return resetUrl; } }, async getResetToken() { const { key, token } = generateResetToken(this._id); this.resetCheckBit = key; await this.save().catch(() => undefined); return token; }, isActive() { return this.status === Status.ACTIVE; }, async setPassword(password) { const key = encrypt(password); const hasLocalStrategy = !!this.getAuthStrategy(STRATEGIES.LOCAL); const strategy = { key, method: STRATEGIES.LOCAL, resetToken: undefined, }; if (hasLocalStrategy) { await this.model('User') .findOneAndUpdate( { _id: this._id, 'strategies.method': STRATEGIES.LOCAL }, { $set: { 'strategies.$': strategy } }, { upsert: true }, ) .catch(); return true; } this.credentials.push(strategy); await this.save().catch(() => false); return true; }, }; AuthSchema.statics = { authenticate: async function (username, password) { const auth = await this.findByUsername(username); if (auth && auth.authenticate(password)) { return auth.record; } return false; }, async findByUsername(username) { return this.findOne({ username }); }, async isUsernameAvailable(username) { return !this.findByUsername(username); }, async resetPassword(token, password) { const decoded = verifyJwt(token); const { sub, key } = decoded as JwtPayload; const auth = await this.findOne({ _id: sub, 'strategies.resetToken': key, }).catch(); if (auth) { await auth.setPassword(password).catch(); return auth.getToken(); } return false; }, }; export type AuthSchema = InferSchemaType;