import { JwtPayload } from 'jsonwebtoken'; import { InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose'; import { Strategy } from './strategy'; import { STRATEGIES } from '../constants/strategies'; import { TokenProps, sign, verify as verifyJwt } from '../utils/jwt'; import { encrypt, verify as verifyPassword } from '../utils/password'; import { generateResetToken } from '../utils/tokens'; import { getPasswordResetLink } from '../utils/links'; export type Auth = { is2FA?: boolean; record: StringSchemaDefinition; username: string; }; export type AuthPrivate = Auth & { 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; setPassword(password: string): Promise; } export interface AuthModel extends Model { authenticate(password: any): boolean; 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: Types.ArraySubdocument, required: true }, 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 sign({ sub: this._id, ...props, }); }, async getResetLink(route) { const token = await this.getResetToken(); if (token) { const resetUrl = getPasswordResetLink(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; }, 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 = { authenticateAndGetRecordLocator: 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(); return !!auth && auth.setPassword(password); }, }; export type AuthSchema = InferSchemaType;