140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
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<Strategy>;
|
|
};
|
|
|
|
export interface AuthMethods {
|
|
authenticate(password: string): boolean;
|
|
getAuthStrategy(method?: STRATEGIES): Strategy | false;
|
|
getResetLink(route: string): Promise<string | undefined>;
|
|
getResetToken(): Promise<string | undefined>;
|
|
getToken(props?: Omit<TokenProps, 'sub'>): string;
|
|
setPassword(password: string): Promise<boolean>;
|
|
}
|
|
|
|
export interface AuthModel extends Model<AuthPrivate, {}, AuthMethods> {
|
|
authenticate(password: any): boolean;
|
|
findByUsername(username: string): Promise<AuthModel & AuthPrivate>;
|
|
isUsernameAvailable(username: string): Promise<boolean>;
|
|
findUserForReset(strategy: STRATEGIES, token: string): Promise<Strategy | undefined>;
|
|
resetPassword(token: string, password: string): Promise<boolean>;
|
|
}
|
|
|
|
export const AuthSchema = new Schema<AuthPrivate, AuthModel, AuthMethods>(
|
|
{
|
|
is2FA: { type: Boolean, default: false },
|
|
record: { type: Types.ObjectId },
|
|
strategies: { type: Types.ArraySubdocument<Strategy>, 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 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;
|
|
}
|
|
},
|
|
|
|
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<typeof AuthSchema>;
|