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'; 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): 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: function (password: string) { const strategy = this.getAuthStrategy(STRATEGIES.LOCAL); return !!strategy && verifyPassword(password, strategy.key); }, getAuthStrategy: function (method = STRATEGIES.LOCAL) { return ( this.strategies .filter((strategy: Strategy) => strategy.method === method) .pop() || false ); }, getToken: function (props = {}) { return sign({ sub: this._id, ...props, }); }, getResetLink: async function (route) { const resetToken = await this.getResetToken(); if (resetToken) { let resetRoute = route; resetRoute = resetRoute.replace(':user_id', this._id); resetRoute = resetRoute.replace(':reset_token?', resetToken); const resetUrl = `${process.env.URL}${resetRoute}`; console.log('[sendPasswordReset] resetUrl:', resetUrl); return resetUrl; } }, getResetToken: async function () { const { key, token } = generateResetToken(this._id); this.resetCheckBit = key; await this.save().catch(() => undefined); return token; }, setPassword: async function (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; // }, findByUsername: async function (username) { return this.findOne({ username }); }, isUsernameAvailable: async function (username) { return !this.findByUsername(username); }, resetPassword: async function (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;