mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
1137 lines
32 KiB
TypeScript
1137 lines
32 KiB
TypeScript
import AuthenticationEmail from "../Utils/AuthenticationEmail";
|
|
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
import { AccountsRoute } from "Common/ServiceRoute";
|
|
import Hostname from "Common/Types/API/Hostname";
|
|
import Protocol from "Common/Types/API/Protocol";
|
|
import Route from "Common/Types/API/Route";
|
|
import URL from "Common/Types/API/URL";
|
|
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
|
|
import OneUptimeDate from "Common/Types/Date";
|
|
import Email from "Common/Types/Email";
|
|
import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
|
|
import BadDataException from "Common/Types/Exception/BadDataException";
|
|
import BadRequestException from "Common/Types/Exception/BadRequestException";
|
|
import { JSONObject } from "Common/Types/JSON";
|
|
import HashedString from "Common/Types/HashedString";
|
|
import Name from "Common/Types/Name";
|
|
import ObjectID from "Common/Types/ObjectID";
|
|
import PositiveNumber from "Common/Types/PositiveNumber";
|
|
import DatabaseConfig from "Common/Server/DatabaseConfig";
|
|
import {
|
|
AppVersion,
|
|
EncryptionSecret,
|
|
IsBillingEnabled,
|
|
} from "Common/Server/EnvironmentConfig";
|
|
import API from "Common/Utils/API";
|
|
import AccessTokenService from "Common/Server/Services/AccessTokenService";
|
|
import EmailVerificationTokenService from "Common/Server/Services/EmailVerificationTokenService";
|
|
import MailService from "Common/Server/Services/MailService";
|
|
import UserService from "Common/Server/Services/UserService";
|
|
import UserTotpAuthService from "Common/Server/Services/UserTotpAuthService";
|
|
import UserSessionService, {
|
|
SessionMetadata,
|
|
} from "Common/Server/Services/UserSessionService";
|
|
import CookieUtil from "Common/Server/Utils/Cookie";
|
|
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
|
|
import Express, {
|
|
ExpressRequest,
|
|
ExpressResponse,
|
|
ExpressRouter,
|
|
NextFunction,
|
|
extractDeviceInfo,
|
|
getClientIp,
|
|
headerValueToString,
|
|
} from "Common/Server/Utils/Express";
|
|
import CaptchaUtil from "Common/Server/Utils/Captcha";
|
|
import logger from "Common/Server/Utils/Logger";
|
|
import Response from "Common/Server/Utils/Response";
|
|
import TotpAuth from "Common/Server/Utils/TotpAuth";
|
|
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
|
|
import User from "Common/Models/DatabaseModels/User";
|
|
import UserSession from "Common/Models/DatabaseModels/UserSession";
|
|
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
|
|
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
|
|
import UserWebAuthnService from "Common/Server/Services/UserWebAuthnService";
|
|
import NotAuthenticatedException from "Common/Types/Exception/NotAuthenticatedException";
|
|
import TeamMemberService from "Common/Server/Services/TeamMemberService";
|
|
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
|
|
|
|
const router: ExpressRouter = Express.getRouter();
|
|
|
|
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
|
|
|
interface FinalizeUserLoginResult {
|
|
sessionMetadata: SessionMetadata;
|
|
accessToken: string;
|
|
}
|
|
|
|
type FinalizeUserLoginInput = {
|
|
req: ExpressRequest;
|
|
res: ExpressResponse;
|
|
user: User;
|
|
isGlobalLogin: boolean;
|
|
};
|
|
|
|
const finalizeUserLogin: (
|
|
data: FinalizeUserLoginInput,
|
|
) => Promise<FinalizeUserLoginResult> = async (
|
|
data: FinalizeUserLoginInput,
|
|
): Promise<FinalizeUserLoginResult> => {
|
|
const { req, res, user, isGlobalLogin } = data;
|
|
|
|
const sessionMetadata: SessionMetadata =
|
|
await UserSessionService.createSession({
|
|
userId: user.id!,
|
|
isGlobalLogin,
|
|
ipAddress: getClientIp(req),
|
|
userAgent: headerValueToString(req.headers["user-agent"]),
|
|
...extractDeviceInfo(req),
|
|
});
|
|
|
|
CookieUtil.setUserCookie({
|
|
expressResponse: res,
|
|
user,
|
|
isGlobalLogin,
|
|
sessionId: sessionMetadata.session.id!,
|
|
refreshToken: sessionMetadata.refreshToken,
|
|
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
|
|
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
|
});
|
|
|
|
// Generate access token for response body (used by mobile clients)
|
|
const accessToken: string = JSONWebToken.signUserLoginToken({
|
|
tokenData: {
|
|
userId: user.id!,
|
|
email: user.email!,
|
|
name: user.name!,
|
|
timezone: user.timezone || null,
|
|
isMasterAdmin: user.isMasterAdmin!,
|
|
isGlobalLogin: isGlobalLogin,
|
|
sessionId: sessionMetadata.session.id!,
|
|
},
|
|
expiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
|
});
|
|
|
|
return { sessionMetadata, accessToken };
|
|
};
|
|
|
|
router.post(
|
|
"/signup",
|
|
async (
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
next: NextFunction,
|
|
): Promise<void> => {
|
|
try {
|
|
if (await DatabaseConfig.shouldDisableSignup()) {
|
|
/*
|
|
* Check if this user has been invited to a project.
|
|
* If so, allow them to sign up even if signup is disabled.
|
|
*/
|
|
const data: JSONObject = req.body["data"] as JSONObject;
|
|
const emailForInviteCheck: string | undefined = data?.["email"] as
|
|
| string
|
|
| undefined;
|
|
|
|
let hasInvitation: boolean = false;
|
|
|
|
if (emailForInviteCheck) {
|
|
const invitedUser: User | null = await UserService.findOneBy({
|
|
query: { email: new Email(emailForInviteCheck) },
|
|
select: { _id: true },
|
|
props: { isRoot: true },
|
|
});
|
|
|
|
if (invitedUser) {
|
|
const pendingInvitation: TeamMember | null =
|
|
await TeamMemberService.findOneBy({
|
|
query: {
|
|
userId: invitedUser.id!,
|
|
hasAcceptedInvitation: false,
|
|
},
|
|
select: { _id: true },
|
|
props: { isRoot: true },
|
|
});
|
|
|
|
if (pendingInvitation) {
|
|
hasInvitation = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasInvitation) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadRequestException(
|
|
"Sign up is disabled on this OneUptime Server. Please contact your server admin to enable it.",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
const miscDataProps: JSONObject =
|
|
(req.body["miscDataProps"] as JSONObject) || {};
|
|
|
|
await CaptchaUtil.verifyCaptcha({
|
|
token:
|
|
(miscDataProps["captchaToken"] as string | undefined) ||
|
|
(req.body["captchaToken"] as string | undefined),
|
|
remoteIp: getClientIp(req) || null,
|
|
});
|
|
|
|
const data: JSONObject = req.body["data"];
|
|
|
|
/* Creating a type that is a partial of the TBaseModel type. */
|
|
const partialUser: User = BaseModel.fromJSON(
|
|
data as JSONObject,
|
|
User,
|
|
) as User;
|
|
|
|
if (IsBillingEnabled) {
|
|
//ALERT: Delete data.role so user don't accidently sign up as master-admin from the API.
|
|
partialUser.isMasterAdmin = false;
|
|
partialUser.isEmailVerified = false;
|
|
} else {
|
|
// IF its not a saas service then we will make the email verified.
|
|
|
|
// check if there are more than one user and if there is then we will not make the user master admin.
|
|
|
|
const userCount: PositiveNumber = await UserService.countBy({
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
partialUser.isMasterAdmin = userCount.isZero(); // if the user count is 0 then make the first user master admin.
|
|
partialUser.isEmailVerified = true;
|
|
}
|
|
|
|
const alreadySavedUser: User | null = await UserService.findOneBy({
|
|
query: { email: partialUser.email as Email },
|
|
select: {
|
|
_id: true,
|
|
password: true,
|
|
timezone: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (alreadySavedUser && alreadySavedUser.password) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(
|
|
`User with email ${partialUser.email} already exists.`,
|
|
),
|
|
);
|
|
}
|
|
|
|
let savedUser: User | null = null;
|
|
|
|
if (alreadySavedUser) {
|
|
savedUser = await UserService.updateOneByIdAndFetch({
|
|
id: alreadySavedUser.id!,
|
|
data: {
|
|
password: partialUser.password!,
|
|
name: partialUser.name!,
|
|
companyPhoneNumber: partialUser.companyPhoneNumber!,
|
|
companyName: partialUser.companyName!,
|
|
},
|
|
select: {
|
|
email: true,
|
|
_id: true,
|
|
name: true,
|
|
isMasterAdmin: true,
|
|
timezone: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
} else {
|
|
const user: User = partialUser;
|
|
|
|
savedUser = await UserService.create({
|
|
data: user,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
const generatedToken: ObjectID = ObjectID.generate();
|
|
|
|
const emailVerificationToken: EmailVerificationToken =
|
|
new EmailVerificationToken();
|
|
emailVerificationToken.userId = savedUser?.id as ObjectID;
|
|
emailVerificationToken.email = savedUser?.email as Email;
|
|
emailVerificationToken.token = generatedToken;
|
|
emailVerificationToken.expires = OneUptimeDate.getOneDayAfter();
|
|
|
|
await EmailVerificationTokenService.create({
|
|
data: emailVerificationToken,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const host: Hostname = await DatabaseConfig.getHost();
|
|
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
|
|
MailService.sendMail({
|
|
toEmail: partialUser.email as Email,
|
|
subject: "Welcome to OneUptime. Please verify your email.",
|
|
templateType: EmailTemplateType.SignupWelcomeEmail,
|
|
vars: {
|
|
name: (partialUser.name! as Name).toString(),
|
|
tokenVerifyUrl: new URL(
|
|
httpProtocol,
|
|
host,
|
|
new Route(AccountsRoute.toString()).addRoute(
|
|
"/verify-email/" + generatedToken.toString(),
|
|
),
|
|
).toString(),
|
|
homeUrl: new URL(httpProtocol, host).toString(),
|
|
},
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
|
|
if (savedUser) {
|
|
// Refresh Permissions for this user here.
|
|
await AccessTokenService.refreshUserAllPermissions(savedUser.id!);
|
|
await finalizeUserLogin({
|
|
req,
|
|
res,
|
|
user: savedUser,
|
|
isGlobalLogin: true,
|
|
});
|
|
|
|
logger.info("User signed up: " + savedUser.email?.toString());
|
|
|
|
if (!IsBillingEnabled && miscDataProps["notifySelfHosted"] === true) {
|
|
const instanceUrl: string = new URL(httpProtocol, host).toString();
|
|
|
|
API.post({
|
|
url: URL.fromString(
|
|
"https://oneuptime.com/api/open-source-deployment/register",
|
|
),
|
|
data: {
|
|
email: savedUser.email?.toString() || "",
|
|
name: savedUser.name?.toString() || "",
|
|
companyName:
|
|
(miscDataProps["selfHostedCompanyName"] as string) || undefined,
|
|
companyPhoneNumber:
|
|
(miscDataProps["selfHostedPhoneNumber"] as string) || undefined,
|
|
oneuptimeVersion: AppVersion,
|
|
instanceUrl: instanceUrl,
|
|
},
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
}
|
|
|
|
return Response.sendEntityResponse(req, res, savedUser, User);
|
|
}
|
|
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadRequestException("Failed to create a user"),
|
|
);
|
|
} catch (err) {
|
|
return next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
router.post(
|
|
"/forgot-password",
|
|
async (
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
next: NextFunction,
|
|
): Promise<void> => {
|
|
try {
|
|
const data: JSONObject = req.body["data"];
|
|
|
|
const user: User = BaseModel.fromJSON(data as JSONObject, User) as User;
|
|
|
|
const alreadySavedUser: User | null = await UserService.findOneBy({
|
|
query: { email: user.email! },
|
|
select: {
|
|
_id: true,
|
|
password: true,
|
|
name: true,
|
|
email: true,
|
|
isMasterAdmin: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (alreadySavedUser && alreadySavedUser.password) {
|
|
const token: string = ObjectID.generate().toString();
|
|
const hashedToken: string = await HashedString.hashValue(
|
|
token,
|
|
EncryptionSecret,
|
|
);
|
|
await UserService.updateOneBy({
|
|
query: {
|
|
_id: alreadySavedUser._id!,
|
|
},
|
|
data: {
|
|
resetPasswordToken: hashedToken,
|
|
resetPasswordExpires: OneUptimeDate.getOneDayAfter(),
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const host: Hostname = await DatabaseConfig.getHost();
|
|
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
|
|
const tokenVerifyUrl: string = new URL(
|
|
httpProtocol,
|
|
host,
|
|
new Route(AccountsRoute.toString()).addRoute(
|
|
"/reset-password/" + token,
|
|
),
|
|
).toString();
|
|
|
|
logger.debug("User forgot password: " + user.email?.toString());
|
|
logger.debug("Reset Password URL: " + tokenVerifyUrl);
|
|
|
|
MailService.sendMail({
|
|
toEmail: user.email!,
|
|
subject: "Password Reset Request for OneUptime",
|
|
templateType: EmailTemplateType.ForgotPassword,
|
|
vars: {
|
|
homeURL: new URL(httpProtocol, host).toString(),
|
|
tokenVerifyUrl: tokenVerifyUrl,
|
|
},
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
}
|
|
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(
|
|
`No user is registered with ${user.email?.toString()}. Please sign up for a new account.`,
|
|
),
|
|
);
|
|
} catch (err) {
|
|
return next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
router.post(
|
|
"/verify-email",
|
|
async (
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
next: NextFunction,
|
|
): Promise<void> => {
|
|
try {
|
|
const data: JSONObject = req.body["data"];
|
|
|
|
const token: EmailVerificationToken = BaseModel.fromJSON(
|
|
data as JSONObject,
|
|
EmailVerificationToken,
|
|
) as EmailVerificationToken;
|
|
|
|
const alreadySavedToken: EmailVerificationToken | null =
|
|
await EmailVerificationTokenService.findOneBy({
|
|
query: { token: token.token! },
|
|
select: {
|
|
_id: true,
|
|
userId: true,
|
|
email: true,
|
|
expires: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!alreadySavedToken) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(
|
|
"Invalid link. Please try to log in and we will resend you another link which you should be able to verify email with.",
|
|
),
|
|
);
|
|
}
|
|
|
|
if (OneUptimeDate.hasExpired(alreadySavedToken.expires!)) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(
|
|
"Link expired. Please try to log in and we will resend you another link which you should be able to verify email with.",
|
|
),
|
|
);
|
|
}
|
|
|
|
const user: User | null = await UserService.findOneBy({
|
|
query: {
|
|
email: alreadySavedToken.email!,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
email: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(
|
|
"Invalid link. Please try to log in and we will resend you another link which you should be able to verify email with.",
|
|
),
|
|
);
|
|
}
|
|
|
|
await UserService.updateOneBy({
|
|
query: {
|
|
_id: user._id!,
|
|
},
|
|
data: {
|
|
isEmailVerified: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const host: Hostname = await DatabaseConfig.getHost();
|
|
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
|
|
MailService.sendMail({
|
|
toEmail: user.email!,
|
|
subject: "Email Verified.",
|
|
templateType: EmailTemplateType.EmailVerified,
|
|
vars: {
|
|
homeURL: new URL(httpProtocol, host).toString(),
|
|
},
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
|
|
logger.info("User email verified: " + user.email?.toString());
|
|
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
} catch (err) {
|
|
return next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
router.post(
|
|
"/reset-password",
|
|
async (
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
next: NextFunction,
|
|
): Promise<void> => {
|
|
try {
|
|
const data: JSONObject = req.body["data"];
|
|
|
|
const user: User = BaseModel.fromJSON(data as JSONObject, User) as User;
|
|
|
|
await user.password?.hashValue(EncryptionSecret);
|
|
|
|
const hashedToken: string = await HashedString.hashValue(
|
|
(user.resetPasswordToken as string) || "",
|
|
EncryptionSecret,
|
|
);
|
|
|
|
const alreadySavedUser: User | null = await UserService.findOneBy({
|
|
query: {
|
|
resetPasswordToken: hashedToken,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
password: true,
|
|
name: true,
|
|
email: true,
|
|
isMasterAdmin: true,
|
|
resetPasswordExpires: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!alreadySavedUser) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(
|
|
"Invalid link. Please go to forgot password page again and request a new link.",
|
|
),
|
|
);
|
|
}
|
|
|
|
if (
|
|
alreadySavedUser &&
|
|
OneUptimeDate.hasExpired(alreadySavedUser.resetPasswordExpires!)
|
|
) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(
|
|
"Expired link. Please go to forgot password page again and request a new link.",
|
|
),
|
|
);
|
|
}
|
|
|
|
await UserService.updateOneById({
|
|
id: alreadySavedUser.id!,
|
|
data: {
|
|
password: user.password!,
|
|
resetPasswordToken: null!,
|
|
resetPasswordExpires: null!,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// Revoke all active sessions for this user on password reset
|
|
await UserSessionService.revokeAllSessionsByUserId(alreadySavedUser.id!, {
|
|
reason: "Password reset",
|
|
});
|
|
|
|
const host: Hostname = await DatabaseConfig.getHost();
|
|
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
|
|
MailService.sendMail({
|
|
toEmail: alreadySavedUser.email!,
|
|
subject: "Password Changed.",
|
|
templateType: EmailTemplateType.PasswordChanged,
|
|
vars: {
|
|
homeURL: new URL(httpProtocol, host).toString(),
|
|
},
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
|
|
logger.info("User password reset: " + alreadySavedUser.email?.toString());
|
|
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
} catch (err) {
|
|
return next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
router.post(
|
|
"/refresh-token",
|
|
async (
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
next: NextFunction,
|
|
): Promise<void> => {
|
|
try {
|
|
// Try cookie first, then fallback to request body (for mobile clients)
|
|
const refreshToken: string | undefined =
|
|
CookieUtil.getRefreshTokenFromExpressRequest(req) ||
|
|
(req.body.refreshToken as string | undefined);
|
|
|
|
if (!refreshToken) {
|
|
CookieUtil.removeAllCookies(req, res);
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotAuthenticatedException(
|
|
"Refresh token missing. Please login again.",
|
|
),
|
|
);
|
|
}
|
|
|
|
const session: UserSession | null =
|
|
await UserSessionService.findActiveSessionByRefreshToken(refreshToken);
|
|
|
|
if (!session || !session.id) {
|
|
CookieUtil.removeAllCookies(req, res);
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotAuthenticatedException("Session expired. Please login again."),
|
|
);
|
|
}
|
|
|
|
if (
|
|
session.refreshTokenExpiresAt &&
|
|
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
|
|
) {
|
|
await UserSessionService.revokeSessionById(session.id, {
|
|
reason: "Refresh token expired",
|
|
});
|
|
CookieUtil.removeAllCookies(req, res);
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotAuthenticatedException("Session expired. Please login again."),
|
|
);
|
|
}
|
|
|
|
if (!session.userId) {
|
|
await UserSessionService.revokeSessionById(session.id, {
|
|
reason: "Session missing user",
|
|
});
|
|
CookieUtil.removeAllCookies(req, res);
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotAuthenticatedException("Session expired. Please login again."),
|
|
);
|
|
}
|
|
|
|
const user: User | null = await UserService.findOneById({
|
|
id: session.userId,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
email: true,
|
|
name: true,
|
|
isMasterAdmin: true,
|
|
profilePictureId: true,
|
|
timezone: true,
|
|
enableTwoFactorAuth: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
await UserSessionService.revokeSessionById(session.id, {
|
|
reason: "User not found",
|
|
});
|
|
CookieUtil.removeAllCookies(req, res);
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotAuthenticatedException("Account no longer exists."),
|
|
);
|
|
}
|
|
|
|
const additionalInfo: JSONObject = (session.additionalInfo ||
|
|
{}) as JSONObject;
|
|
const isGlobalLogin: boolean =
|
|
typeof additionalInfo["isGlobalLogin"] === "boolean"
|
|
? (additionalInfo["isGlobalLogin"] as boolean)
|
|
: true;
|
|
|
|
const renewedSession: SessionMetadata =
|
|
await UserSessionService.renewSessionWithNewRefreshToken({
|
|
session,
|
|
ipAddress: getClientIp(req),
|
|
userAgent: headerValueToString(req.headers["user-agent"]),
|
|
...extractDeviceInfo(req),
|
|
});
|
|
|
|
CookieUtil.setUserCookie({
|
|
expressResponse: res,
|
|
user,
|
|
isGlobalLogin,
|
|
sessionId: renewedSession.session.id!,
|
|
refreshToken: renewedSession.refreshToken,
|
|
refreshTokenExpiresAt: renewedSession.refreshTokenExpiresAt,
|
|
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
|
});
|
|
|
|
// Generate access token for response body (used by mobile clients)
|
|
const newAccessToken: string = JSONWebToken.signUserLoginToken({
|
|
tokenData: {
|
|
userId: user.id!,
|
|
email: user.email!,
|
|
name: user.name!,
|
|
timezone: user.timezone || null,
|
|
isMasterAdmin: user.isMasterAdmin!,
|
|
isGlobalLogin: isGlobalLogin,
|
|
sessionId: renewedSession.session.id!,
|
|
},
|
|
expiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
|
});
|
|
|
|
return Response.sendJsonObjectResponse(req, res, {
|
|
accessToken: newAccessToken,
|
|
refreshToken: renewedSession.refreshToken,
|
|
refreshTokenExpiresAt:
|
|
renewedSession.refreshTokenExpiresAt.toISOString(),
|
|
});
|
|
} catch (err) {
|
|
return next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
router.post(
|
|
"/logout",
|
|
async (
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
next: NextFunction,
|
|
): Promise<void> => {
|
|
try {
|
|
// Try cookie first, then fallback to request body (for mobile clients)
|
|
const refreshToken: string | undefined =
|
|
CookieUtil.getRefreshTokenFromExpressRequest(req) ||
|
|
(req.body.refreshToken as string | undefined);
|
|
|
|
if (refreshToken) {
|
|
await UserSessionService.revokeSessionByRefreshToken(refreshToken, {
|
|
reason: "User logout",
|
|
});
|
|
}
|
|
|
|
CookieUtil.removeAllCookies(req, res);
|
|
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
} catch (err) {
|
|
return next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
router.post(
|
|
"/verify-totp-auth",
|
|
async (
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
next: NextFunction,
|
|
): Promise<void> => {
|
|
return login({
|
|
req: req,
|
|
res: res,
|
|
next: next,
|
|
verifyTotpAuth: true,
|
|
verifyWebAuthn: false,
|
|
});
|
|
},
|
|
);
|
|
|
|
router.post(
|
|
"/verify-webauthn-auth",
|
|
async (
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
next: NextFunction,
|
|
): Promise<void> => {
|
|
return login({
|
|
req: req,
|
|
res: res,
|
|
next: next,
|
|
verifyTotpAuth: false,
|
|
verifyWebAuthn: true,
|
|
});
|
|
},
|
|
);
|
|
|
|
router.post(
|
|
"/login",
|
|
async (
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
next: NextFunction,
|
|
): Promise<void> => {
|
|
return login({
|
|
req: req,
|
|
res: res,
|
|
next: next,
|
|
verifyTotpAuth: false,
|
|
verifyWebAuthn: false,
|
|
});
|
|
},
|
|
);
|
|
|
|
type FetchTotpAuthListFunction = (userId: ObjectID) => Promise<{
|
|
totpAuthList: Array<UserTotpAuth>;
|
|
webAuthnList: Array<UserWebAuthn>;
|
|
}>;
|
|
|
|
const fetchTotpAuthList: FetchTotpAuthListFunction = async (
|
|
userId: ObjectID,
|
|
): Promise<{
|
|
totpAuthList: Array<UserTotpAuth>;
|
|
webAuthnList: Array<UserWebAuthn>;
|
|
}> => {
|
|
const totpAuthList: Array<UserTotpAuth> = await UserTotpAuthService.findBy({
|
|
query: {
|
|
userId: userId,
|
|
isVerified: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
userId: true,
|
|
name: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const webAuthnList: Array<UserWebAuthn> = await UserWebAuthnService.findBy({
|
|
query: {
|
|
userId: userId,
|
|
isVerified: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
userId: true,
|
|
name: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return {
|
|
totpAuthList: totpAuthList || [],
|
|
webAuthnList: webAuthnList || [],
|
|
};
|
|
};
|
|
|
|
type LoginFunction = (options: {
|
|
req: ExpressRequest;
|
|
res: ExpressResponse;
|
|
next: NextFunction;
|
|
verifyTotpAuth: boolean;
|
|
verifyWebAuthn: boolean;
|
|
}) => Promise<void>;
|
|
|
|
const login: LoginFunction = async (options: {
|
|
req: ExpressRequest;
|
|
res: ExpressResponse;
|
|
next: NextFunction;
|
|
verifyTotpAuth: boolean;
|
|
verifyWebAuthn: boolean;
|
|
}): Promise<void> => {
|
|
const req: ExpressRequest = options.req;
|
|
const res: ExpressResponse = options.res;
|
|
const next: NextFunction = options.next;
|
|
const verifyTotpAuth: boolean = options.verifyTotpAuth;
|
|
const verifyWebAuthn: boolean = options.verifyWebAuthn;
|
|
|
|
try {
|
|
const miscDataProps: JSONObject =
|
|
(req.body["miscDataProps"] as JSONObject) || {};
|
|
|
|
if (!verifyTotpAuth && !verifyWebAuthn) {
|
|
await CaptchaUtil.verifyCaptcha({
|
|
token:
|
|
(miscDataProps["captchaToken"] as string | undefined) ||
|
|
(req.body["captchaToken"] as string | undefined),
|
|
remoteIp: getClientIp(req) || null,
|
|
});
|
|
}
|
|
|
|
const data: JSONObject = req.body["data"];
|
|
|
|
logger.debug("Login request data: " + JSON.stringify(req.body, null, 2));
|
|
|
|
const user: User = BaseModel.fromJSON(data as JSONObject, User) as User;
|
|
|
|
if (!user.email || !user.password) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException("Email and password are required."),
|
|
);
|
|
}
|
|
|
|
await user.password?.hashValue(EncryptionSecret);
|
|
|
|
const alreadySavedUser: User | null = await UserService.findOneBy({
|
|
query: { email: user.email! },
|
|
select: {
|
|
_id: true,
|
|
password: true,
|
|
name: true,
|
|
email: true,
|
|
isMasterAdmin: true,
|
|
isEmailVerified: true,
|
|
profilePictureId: true,
|
|
timezone: true,
|
|
enableTwoFactorAuth: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (alreadySavedUser) {
|
|
if (!alreadySavedUser.password) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(
|
|
"You have not signed up so far. Please go to the registration page to sign up.",
|
|
),
|
|
);
|
|
}
|
|
|
|
if (!alreadySavedUser.isEmailVerified) {
|
|
await AuthenticationEmail.sendVerificationEmail(alreadySavedUser);
|
|
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(
|
|
"Email is not verified. We have sent you an email with the verification link. Please do not forget to check spam.",
|
|
),
|
|
);
|
|
}
|
|
|
|
if (alreadySavedUser.password.toString() !== user.password!.toString()) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(
|
|
"Invalid login: Email or password does not match.",
|
|
),
|
|
);
|
|
}
|
|
|
|
if (
|
|
alreadySavedUser.enableTwoFactorAuth &&
|
|
!verifyTotpAuth &&
|
|
!verifyWebAuthn
|
|
) {
|
|
// If two factor auth is enabled then we will send the user to the two factor auth page.
|
|
|
|
const { totpAuthList, webAuthnList } = await fetchTotpAuthList(
|
|
alreadySavedUser.id!,
|
|
);
|
|
|
|
if (
|
|
(!totpAuthList || totpAuthList.length === 0) &&
|
|
(!webAuthnList || webAuthnList.length === 0)
|
|
) {
|
|
const errorMessage: string = IsBillingEnabled
|
|
? "Two Factor Authentication is enabled but no two factor auth is setup. Please contact OneUptime support for help."
|
|
: "Two Factor Authentication is enabled but no two factor auth is setup. Please contact your server admin to disable two factor auth for this account.";
|
|
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException(errorMessage),
|
|
);
|
|
}
|
|
|
|
return Response.sendEntityResponse(req, res, alreadySavedUser, User, {
|
|
miscData: {
|
|
totpAuthList: UserTotpAuth.toJSONArray(totpAuthList, UserTotpAuth),
|
|
webAuthnList: UserWebAuthn.toJSONArray(webAuthnList, UserWebAuthn),
|
|
},
|
|
});
|
|
}
|
|
|
|
if (verifyTotpAuth || verifyWebAuthn) {
|
|
if (verifyTotpAuth) {
|
|
// code from req
|
|
const code: string = data["code"] as string;
|
|
const twoFactorAuthId: string = data["twoFactorAuthId"] as string;
|
|
|
|
const totpAuth: UserTotpAuth | null =
|
|
await UserTotpAuthService.findOneBy({
|
|
query: {
|
|
_id: twoFactorAuthId,
|
|
userId: alreadySavedUser.id!,
|
|
isVerified: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
twoFactorSecret: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!totpAuth) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException("Invalid two factor auth id."),
|
|
);
|
|
}
|
|
|
|
const isVerified: boolean = TotpAuth.verifyToken({
|
|
token: code,
|
|
secret: totpAuth.twoFactorSecret!,
|
|
email: alreadySavedUser.email!,
|
|
});
|
|
|
|
if (!isVerified) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException("Invalid code."),
|
|
);
|
|
}
|
|
} else if (verifyWebAuthn) {
|
|
const credential: any = data["credential"];
|
|
|
|
await UserWebAuthnService.verifyAuthentication({
|
|
userId: alreadySavedUser.id!.toString(),
|
|
credential: credential,
|
|
});
|
|
}
|
|
} // Refresh Permissions for this user here.
|
|
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
|
|
|
|
if (alreadySavedUser.password.toString() === user.password!.toString()) {
|
|
logger.info("User logged in: " + alreadySavedUser.email?.toString());
|
|
|
|
const loginResult: FinalizeUserLoginResult = await finalizeUserLogin({
|
|
req,
|
|
res,
|
|
user: alreadySavedUser,
|
|
isGlobalLogin: true,
|
|
});
|
|
|
|
return Response.sendEntityResponse(req, res, alreadySavedUser, User, {
|
|
miscData: {
|
|
accessToken: loginResult.accessToken,
|
|
refreshToken: loginResult.sessionMetadata.refreshToken,
|
|
refreshTokenExpiresAt:
|
|
loginResult.sessionMetadata.refreshTokenExpiresAt.toISOString(),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException("Invalid login: Email or password does not match."),
|
|
);
|
|
} catch (err) {
|
|
return next(err);
|
|
}
|
|
};
|
|
|
|
export default router;
|