mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-18 23:48:43 -05:00
recovery
This commit is contained in:
@@ -73,12 +73,14 @@ import {
|
||||
MongoStatusPagesRepository,
|
||||
MongoUsersRepository,
|
||||
MongoInvitesRepository,
|
||||
MongoRecoveryTokensRepository,
|
||||
IMonitorsRepository,
|
||||
IChecksRepository,
|
||||
IMonitorStatsRepository,
|
||||
IStatusPagesRepository,
|
||||
IUsersRepository,
|
||||
IInvitesRepository,
|
||||
IRecoveryTokensRepository,
|
||||
} from "@/repositories/index.js";
|
||||
|
||||
export type InitializedSerivces = {
|
||||
@@ -110,6 +112,7 @@ export type InitializedSerivces = {
|
||||
statusPagesRepository: IStatusPagesRepository;
|
||||
usersRepository: IUsersRepository;
|
||||
invitesRepository: IInvitesRepository;
|
||||
recoveryTokensRepository: IRecoveryTokensRepository;
|
||||
};
|
||||
|
||||
export const initializeServices = async ({
|
||||
@@ -163,6 +166,7 @@ export const initializeServices = async ({
|
||||
const statusPagesRepository = new MongoStatusPagesRepository();
|
||||
const usersRepository = new MongoUsersRepository();
|
||||
const invitesRepository = new MongoInvitesRepository();
|
||||
const recoveryTokensRepository = new MongoRecoveryTokensRepository();
|
||||
|
||||
const networkService = new NetworkService({
|
||||
axios,
|
||||
@@ -247,6 +251,7 @@ export const initializeServices = async ({
|
||||
monitorsRepository,
|
||||
usersRepository,
|
||||
invitesRepository,
|
||||
recoveryTokensRepository,
|
||||
});
|
||||
|
||||
const diagnosticService = new DiagnosticService();
|
||||
@@ -307,6 +312,7 @@ export const initializeServices = async ({
|
||||
statusPagesRepository,
|
||||
usersRepository,
|
||||
invitesRepository,
|
||||
recoveryTokensRepository,
|
||||
};
|
||||
|
||||
Object.values(services).forEach((service) => {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const RecoveryTokenSchema = mongoose.Schema(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 600,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default mongoose.model("RecoveryToken", RecoveryTokenSchema);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Schema, model, type Types } from "mongoose";
|
||||
import type { RecoveryToken as RecoveryTokenEntity } from "@/types/recoveryToken.js";
|
||||
|
||||
type RecoveryTokenDocumentBase = Omit<RecoveryTokenEntity, "id" | "createdAt" | "updatedAt" | "expiry"> & {
|
||||
expiry: Date;
|
||||
};
|
||||
|
||||
interface RecoveryTokenDocument extends RecoveryTokenDocumentBase {
|
||||
_id: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const RecoveryTokenSchema = new Schema<RecoveryTokenDocument>(
|
||||
{
|
||||
email: { type: String, required: true, unique: true },
|
||||
token: { type: String, required: true },
|
||||
expiry: { type: Date, default: Date.now, expires: 600 },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const RecoveryTokenModel = model<RecoveryTokenDocument>("RecoveryToken", RecoveryTokenSchema);
|
||||
|
||||
export type { RecoveryTokenDocument };
|
||||
export { RecoveryTokenModel };
|
||||
export default RecoveryTokenModel;
|
||||
@@ -15,3 +15,6 @@ export { default as UserModel } from "@/db/models/User.js";
|
||||
|
||||
export * from "@/db/models/Invite.js";
|
||||
export { default as InviteModel } from "@/db/models/Invite.js";
|
||||
|
||||
export * from "@/db/models/RecoveryToken.js";
|
||||
export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js";
|
||||
|
||||
@@ -15,3 +15,6 @@ export { default as MongoUsersRepository } from "@/repositories/users/MongoUsers
|
||||
|
||||
export * from "@/repositories/invites/IInvitesRepository.js";
|
||||
export { default as MongoInvitesRepository } from "@/repositories/invites/MongoInviteRepository.js";
|
||||
|
||||
export * from "@/repositories/recovery-tokens/IRecoveryTokensRepository.js";
|
||||
export { default as MongoRecoveryTokensRepository } from "@/repositories/recovery-tokens/MongoRecoveryTokensRepository.js";
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { RecoveryToken } from "@/types/recoveryToken.js";
|
||||
|
||||
export interface IRecoveryTokensRepository {
|
||||
// create
|
||||
create(email: string): Promise<RecoveryToken>;
|
||||
// fetch
|
||||
findByToken(token: string): Promise<RecoveryToken>;
|
||||
// update
|
||||
// delete
|
||||
deleteManyByEmail(email: string): Promise<number>;
|
||||
// other
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { RecoveryToken } from "@/types/index.js";
|
||||
import type { IRecoveryTokensRepository } from "./IRecoveryTokensRepository.js";
|
||||
import type { RecoveryTokenDocument } from "@/db/models/RecoveryToken.js";
|
||||
import { RecoveryTokenModel } from "@/db/models/RecoveryToken.js";
|
||||
import mongoose from "mongoose";
|
||||
import crypto from "crypto";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
const SERVICE_NAME = "MongoRecoveryTokensRepository";
|
||||
|
||||
class MongoRecoveryTokensRepository implements IRecoveryTokensRepository {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
|
||||
};
|
||||
|
||||
private toDateString = (value?: Date | string | null): string => {
|
||||
if (!value) {
|
||||
return new Date(0).toISOString();
|
||||
}
|
||||
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
||||
};
|
||||
|
||||
protected toEntity = (doc: RecoveryTokenDocument): RecoveryToken => {
|
||||
return {
|
||||
id: this.toStringId(doc._id),
|
||||
email: doc.email,
|
||||
token: doc.token,
|
||||
expiry: this.toDateString(doc.expiry),
|
||||
createdAt: this.toDateString(doc.createdAt),
|
||||
updatedAt: this.toDateString(doc.updatedAt),
|
||||
};
|
||||
};
|
||||
|
||||
create = async (email: string): Promise<RecoveryToken> => {
|
||||
const token = await RecoveryTokenModel.create({
|
||||
email,
|
||||
token: crypto.randomBytes(32).toString("hex"),
|
||||
});
|
||||
return this.toEntity(token);
|
||||
};
|
||||
|
||||
findByToken = async (token: string): Promise<RecoveryToken> => {
|
||||
const recoveryToken = await RecoveryTokenModel.findOne({ token });
|
||||
if (!recoveryToken) {
|
||||
throw new AppError({ message: "Recovery token not found", service: SERVICE_NAME, status: 404 });
|
||||
}
|
||||
return this.toEntity(recoveryToken);
|
||||
};
|
||||
|
||||
deleteManyByEmail = async (email: string) => {
|
||||
const result = await RecoveryTokenModel.deleteMany({
|
||||
email,
|
||||
});
|
||||
return Promise.resolve(result.deletedCount || 0);
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoRecoveryTokensRepository;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { IInvitesRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
||||
import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository } from "@/repositories/index.js";
|
||||
import Team from "@/db/models/Team.js";
|
||||
import type { User } from "@/types/index.js";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
const SERVICE_NAME = "userService";
|
||||
|
||||
@@ -20,6 +21,7 @@ class UserService {
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private usersRepository: IUsersRepository;
|
||||
private invitesRepository: IInvitesRepository;
|
||||
private recoveryTokensRepository: IRecoveryTokensRepository;
|
||||
|
||||
constructor({
|
||||
crypto,
|
||||
@@ -34,6 +36,7 @@ class UserService {
|
||||
monitorsRepository,
|
||||
usersRepository,
|
||||
invitesRepository,
|
||||
recoveryTokensRepository,
|
||||
}: {
|
||||
crypto: any;
|
||||
db: any;
|
||||
@@ -47,6 +50,7 @@ class UserService {
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
usersRepository: IUsersRepository;
|
||||
invitesRepository: IInvitesRepository;
|
||||
recoveryTokensRepository: IRecoveryTokensRepository;
|
||||
}) {
|
||||
this.db = db;
|
||||
this.emailService = emailService;
|
||||
@@ -60,6 +64,7 @@ class UserService {
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.usersRepository = usersRepository;
|
||||
this.invitesRepository = invitesRepository;
|
||||
this.recoveryTokensRepository = recoveryTokensRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -178,13 +183,15 @@ class UserService {
|
||||
};
|
||||
|
||||
checkSuperadminExists = async () => {
|
||||
const superAdminExists = await this.db.userModule.checkSuperadmin();
|
||||
return superAdminExists;
|
||||
return await this.usersRepository.findSuperAdmin();
|
||||
};
|
||||
|
||||
requestRecovery = async (email: string) => {
|
||||
const user = await this.db.userModule.getUserByEmail(email);
|
||||
const recoveryToken = await this.db.recoveryModule.requestRecoveryToken(email);
|
||||
|
||||
// Delete existing tokens
|
||||
await this.recoveryTokensRepository.deleteManyByEmail(email);
|
||||
const recoveryToken = await this.recoveryTokensRepository.create(email);
|
||||
const name = user.firstName;
|
||||
const { clientHost } = this.settingsService.getSettings();
|
||||
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
|
||||
@@ -199,14 +206,29 @@ class UserService {
|
||||
};
|
||||
|
||||
validateRecovery = async (recoveryToken: string) => {
|
||||
await this.db.recoveryModule.validateRecoveryToken(recoveryToken);
|
||||
// Throws if token not found, validating
|
||||
await this.recoveryTokensRepository.findByToken(recoveryToken);
|
||||
};
|
||||
|
||||
resetPassword = async (password: string, recoveryToken: string) => {
|
||||
const user = await this.db.recoveryModule.resetPassword(password, recoveryToken);
|
||||
const appSettings = await this.settingsService.getSettings();
|
||||
const token = this.issueToken(user._doc, appSettings);
|
||||
return { user, token };
|
||||
const existingToken = await this.recoveryTokensRepository.findByToken(recoveryToken);
|
||||
const existingUser = await this.usersRepository.findByEmail(existingToken.email);
|
||||
|
||||
const match = await bcrypt.compare(password, existingUser.password);
|
||||
if (match === true) {
|
||||
throw new AppError({ message: "New password cannot be same as old password", service: SERVICE_NAME, status: 400 });
|
||||
}
|
||||
|
||||
existingUser.password = password;
|
||||
await this.usersRepository.updateById(existingUser.id, existingUser, null);
|
||||
await this.recoveryTokensRepository.deleteManyByEmail(existingUser.email);
|
||||
|
||||
existingUser.password = "";
|
||||
existingUser.profileImage = undefined;
|
||||
|
||||
const token = this.issueToken(existingUser, await this.settingsService.getSettings());
|
||||
|
||||
return { user: existingUser, token };
|
||||
};
|
||||
|
||||
deleteUser = async (user: any) => {
|
||||
|
||||
@@ -46,7 +46,7 @@ class EmailService {
|
||||
*/
|
||||
this.loadTemplate = (templateName) => {
|
||||
try {
|
||||
const templatePath = this.path.join(__dirname, `../../../templates/${templateName}.mjml`);
|
||||
const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`);
|
||||
const templateContent = this.fs.readFileSync(templatePath, "utf8");
|
||||
return this.compile(templateContent);
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from "@/types/statusPage.js";
|
||||
export * from "@/types/network.js";
|
||||
export * from "@/types/user.js";
|
||||
export * from "@/types/invite.js";
|
||||
export * from "@/types/recoveryToken.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserRole } from "./user.js";
|
||||
import type { UserRole } from "@/types/user.js";
|
||||
|
||||
export interface Invite {
|
||||
id: string;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface RecoveryToken {
|
||||
id: string;
|
||||
email: string;
|
||||
token: string;
|
||||
expiry: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user