fix accounts page

This commit is contained in:
Simon Larsen 2022-11-11 18:03:17 +00:00
parent 30d9242e2d
commit addfc9d69c
No known key found for this signature in database
GPG Key ID: AB45983AA9C81CDE
6 changed files with 730 additions and 19 deletions

View File

@ -16,6 +16,7 @@ import 'CommonUI/src/Styles/theme.scss';
import Navigation from 'CommonUI/src/Utils/Navigation';
import VerifyEmail from './Pages/VerifyEmail';
import User from 'CommonUI/src/Utils/User';
import ResetPasswordPage from './Pages/ResetPassword';
function App(): ReactElement {
Navigation.setNavigateHook(useNavigate());
@ -37,7 +38,7 @@ function App(): ReactElement {
/>
<Route
path="/accounts/reset-password/:token"
element={<ForgotPasswordPage />}
element={<ResetPasswordPage />}
/>
<Route path="/accounts/register" element={<RegisterPage />} />
<Route path="/accounts/login/sso" element={<SsoLoginPage />} />

View File

@ -0,0 +1,134 @@
import React, { FunctionComponent, useState } from 'react';
import ModelForm, { FormType } from 'CommonUI/src/Components/Forms/ModelForm';
import User from 'Model/Models/User';
import Link from 'CommonUI/src/Components/Link/Link';
import Route from 'Common/Types/API/Route';
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
import OneUptimeLogo from 'CommonUI/src/Images/logos/OneUptimePNG/7.png';
import URL from 'Common/Types/API/URL';
import { RESET_PASSWORD_API_URL } from '../Utils/ApiPaths';
import Navigation from 'CommonUI/src/Utils/Navigation';
const RegisterPage: FunctionComponent = () => {
const apiUrl: URL = RESET_PASSWORD_API_URL;
const [isSuccess, setIsSuccess] = useState<boolean>(false);
return (
<div className="auth-page">
<div className="container-fluid p-0">
<div className="row g-0">
<div className="col-xxl-3 col-lg-3 col-md-2"></div>
<div className="col-xxl-6 col-lg-6 col-md-8">
<div className="auth-full-page-content d-flex p-sm-5 p-4">
<div className="w-100">
<div className="d-flex flex-column h-100">
<div className="auth-content my-auto">
<div
className="mt-4 text-center"
style={{ marginBottom: '40px' }}
>
<img
style={{ height: '40px' }}
src={`/accounts/public/${OneUptimeLogo}`}
/>
</div>
<div className="text-center">
<h5 className="mb-0">
Reset Password.
</h5>
{!isSuccess && <p className="text-muted mt-2 mb-0">
Please enter your new password and we will have it updated.{' '}
</p>}
{isSuccess && <p className="text-muted mt-2 mb-0">
Your password has been updated. Please log in.
</p>}
</div>
{!isSuccess && <ModelForm<User>
modelType={User}
id="register-form"
onBeforeCreate={(item: User) => {
item.resetPasswordToken = Navigation.getLastParam()?.toString().replace("/", "").toString() ||''
return item;
}}
showAsColumns={1}
maxPrimaryButtonWidth={true}
initialValues={{
password: '',
confirmPassword: '',
}}
fields={[
{
field: {
password: true,
},
fieldType:
FormFieldSchemaType.Password,
validation: {
minLength: 6,
},
placeholder: 'New Password',
title: 'New Password',
required: true,
},
{
field: {
password: true,
},
validation: {
minLength: 6,
toMatchField:
'password',
},
fieldType:
FormFieldSchemaType.Password,
placeholder:
'Confirm Password',
title: 'Confirm Password',
overideFieldKey:
'confirmPassword',
required: true,
},
]}
apiUrl={apiUrl}
formType={FormType.Create}
submitButtonText={'Reset Password'}
onSuccess={() => {
setIsSuccess(true);
}}
/>}
<div className="mt-5 text-center">
<p className="text-muted mb-0">
Know your password?{' '}
<Link
to={
new Route(
'/accounts/login'
)
}
className="underline-on-hover text-primary fw-semibold"
>
Log in.
</Link>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="col-xxl-3 col-lg-3 col-md-2"></div>
</div>
</div>
</div>
);
};
export default RegisterPage;

View File

@ -15,4 +15,8 @@ export const FORGOT_PASSWORD_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
export const VERIFY_EMAIL_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route('/verify-email')
);
export const RESET_PASSWORD_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route('/reset-password')
);

View File

@ -2,6 +2,7 @@ enum EmailTemplateType {
ForgotPassword = 'ForgotPassword.hbs',
SignupWelcomeEmail = 'SignupWelcomeEmail.hbs',
EmailVerified = 'EmailVerified.hbs',
PasswordChanged = 'PasswordChanged.hbs',
}
export default EmailTemplateType;

View File

@ -106,7 +106,7 @@ router.post(
const emailVerificationToken = new EmailVerificationToken();
emailVerificationToken.userId = savedUser?.id!;
emailVerificationToken.email = savedUser?.email!;
emailVerificationToken.token = generatedToken;
emailVerificationToken.token = generatedToken;
emailVerificationToken.expires = OneUptimeDate.getOneDayAfter()
await EmailVerificationTokenService.create({
@ -321,33 +321,54 @@ router.post(
await user.password?.hashValue(EncryptionSecret);
const alreadySavedUser: User | null = await UserService.findOneBy({
query: { email: user.email!, password: user.password! },
query: { resetPasswordToken: user.resetPasswordToken as string || '' },
select: {
_id: true,
password: true,
name: true,
email: true,
isMasterAdmin: true,
resetPasswordExpires: true
},
props: {
isRoot: true,
},
});
if (alreadySavedUser) {
const token: string = JSONWebToken.sign(
alreadySavedUser,
OneUptimeDate.getSecondsInDays(new PositiveNumber(30))
);
return Response.sendJsonObjectResponse(req, res, {
token: token,
user: BaseModel.toJSON(alreadySavedUser, User),
});
if (!alreadySavedUser) {
throw new BadDataException("Invalid link. Please go to forgot password page again and request a new link.")
}
throw new BadDataException(
'Invalid login: Email or password does not match.'
);
if (alreadySavedUser && OneUptimeDate.hasExpired(alreadySavedUser.resetPasswordExpires!)) {
throw 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,
},
});
MailService.sendMail({
toEmail: alreadySavedUser.email!,
subject: 'Password Changed.',
templateType: EmailTemplateType.PasswordChanged,
vars: {
homeURL: new URL(HttpProtocol, Domain).toString(),
},
}).catch((err: Error) => {
logger.error(err);
});
return Response.sendEmptyResponse(req, res);
} catch (err) {
return next(err);
}
@ -391,16 +412,16 @@ router.post(
const emailVerificationToken = new EmailVerificationToken();
emailVerificationToken.userId = alreadySavedUser?.id!;
emailVerificationToken.email = alreadySavedUser?.email!;
emailVerificationToken.token = generatedToken;
emailVerificationToken.token = generatedToken;
emailVerificationToken.expires = OneUptimeDate.getOneDayAfter()
await EmailVerificationTokenService.create({
data: emailVerificationToken,
props: {
isRoot: true
},
});
MailService.sendMail({
toEmail: alreadySavedUser.email!,

View File

@ -0,0 +1,550 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=3Dutf-8">
<meta name="viewport" content="width=3Ddevice-width">
<title>Welcome to OneUptime.</title>
<style>
/**
* IMPORTANT:
* Please read before changing anything, CSS involved in our HTML emails is
* extremely specific and written a certain way for a reason. It might not make
* sense in a normal setting but Outlook loves it this way.
*
* !!! [override] prevents Yahoo Mail breaking media queries. It must be used
* !!! at the beginning of every line of CSS inside a media query.
* !!! Do not remove.
*
* !!! div[style*="margin: 16px 0"] allows us to target a weird margin
* !!! bug in Android's email client.
* !!! Do not remove.
*
* Also, the img files are hosted on S3. Please don't break these URLs!
* The images are also versioned by date, so please update the URLs accordingly
* if you create new versions
*
***/
/**
* # Root
* - CSS resets and general styles go here.
**/
html,
body,
a,
span,
div[style*="margin: 16px 0"] {
border: 0 !important;
margin: 0 !important;
outline: 0 !important;
padding: 0 !important;
text-decoration: none !important;
}
a,
span,
td,
th {
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
/**
* # Delink
* - Classes for overriding clients which creates links out of things like
* emails, addresses, phone numbers, etc.
**/
span.st-Delink a {
color: #000000 !important;
text-decoration: none !important;
}
/** Modifier: preheader */
span.st-Delink.st-Delink--preheader a {
color: white !important;
text-decoration: none !important;
}
/** */
/** Modifier: title */
span.st-Delink.st-Delink--title a {
color: #000000 !important;
text-decoration: none !important;
}
/** */
/** Modifier: footer */
span.st-Delink.st-Delink--footer a {
color: #8898aa !important;
text-decoration: none !important;
}
/** */
.ii a[href] {
color: #000000;
}
/**
* # Mobile
* - This affects emails views in clients less than 600px wide.
**/
@media all and (max-width: 600px) {
/**
* # Wrapper
**/
body[override] table.st-Wrapper,
body[override] table.st-Width.st-Width--mobile {
min-width: 100% !important;
width: 100% !important;
}
/**
* # Spacer
**/
/** Modifier: gutter */
body[override] td.st-Spacer.st-Spacer--gutter {
width: 32px !important;
}
/** */
/** Modifier: kill */
body[override] td.st-Spacer.st-Spacer--kill {
width: 0 !important;
}
/** */
/** Modifier: emailEnd */
body[override] td.st-Spacer.st-Spacer--emailEnd {
height: 32px !important;
}
/** */
/**
* # Font
**/
/** Modifier: title */
body[override] td.st-Font.st-Font--title,
body[override] td.st-Font.st-Font--title span,
body[override] td.st-Font.st-Font--title a {
font-size: 28px !important;
line-height: 36px !important;
}
/** */
/** Modifier: header */
body[override] td.st-Font.st-Font--header,
body[override] td.st-Font.st-Font--header span,
body[override] td.st-Font.st-Font--header a {
font-size: 24px !important;
line-height: 32px !important;
}
/** */
/** Modifier: body */
body[override] td.st-Font.st-Font--body,
body[override] td.st-Font.st-Font--body span,
body[override] td.st-Font.st-Font--body a {
font-size: 18px !important;
line-height: 28px !important;
}
/** */
/** Modifier: caption */
body[override] td.st-Font.st-Font--caption,
body[override] td.st-Font.st-Font--caption span,
body[override] td.st-Font.st-Font--caption a {
font-size: 14px !important;
line-height: 20px !important;
}
/** */
/** Modifier: simplified */
body[override] table.st-Header.st-Header--simplified td.st-Header-logo {
width: auto !important;
}
body[override] table.st-Header.st-Header--simplified td.st-Header-spacing {
width: 0 !important;
}
/**
* # Divider
**/
body[override] table.st-Divider td.st-Spacer.st-Spacer--gutter,
body[override] tr.st-Divider td.st-Spacer.st-Spacer--gutter {
background-color: #000000;
}
/**
* # Blocks
**/
body[override] table.st-Blocks table.st-Blocks-inner {
border-radius: 0 !important;
}
body[override] table.st-Blocks table.st-Blocks-inner table.st-Blocks-item td.st-Blocks-item-cell {
display: block !important;
}
/**
* # Button
**/
body[override] table.st-Button {
margin: 0 auto !important;
width: 100% !important;
}
body[override] table.st-Button td.st-Button-area,
body[override] table.st-Button td.st-Button-area a.st-Button-link,
body[override] table.st-Button td.st-Button-area span.st-Button-internal {
height: 44px !important;
line-height: 44px !important;
font-size: 18px !important;
vertical-align: middle !important;
}
}
</style>
</head>
<body class="st-Email" bgcolor="f7f7f7"
style="border: 0; margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; min-width: 100%; width: 100%;"
override="fix">
<!-- Background -->
<table class="st-Background" bgcolor="f7f7f7" border="0" cellpadding="0" cellspacing="0" width="100%"
style="border: 0; margin: 0; padding: 0;">
<tbody>
<tr>
<td style="border: 0; margin: 0; padding: 0;">
<!-- Wrapper -->
<table class="st-Wrapper" align="center" bgcolor="ffffff" border="0" cellpadding="0" cellspacing="0"
width="600"
style="border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; margin: 0 auto; min-width: 600px;">
<tbody>
<tr>
<td style="border: 0; margin: 0; padding: 0;">
<table class="st-Header st-Header--simplified st-Width st-Width--mobile" border="0" cellpadding="0"
cellspacing="0" width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--divider" colspan="4" height="19"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td align="left" style="height:80px; border: 0; margin: 0; padding: 0;">
<div>
<a style="border: 0; margin: 0; padding: 0; text-decoration: none;" href={{homeURL}}>
<img alt="OneUptime" border="0"
style="height:70px; width:70px; border: 0; margin: 0; padding: 0; color: #000000; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; font-weight: normal;"
src="https://res.cloudinary.com/deityhub/image/upload/v1637736803/1png.png">
</a>
</div>
</td>
<td class="st-Header-spacing" width="423" style="border: 0; margin: 0; padding: 0;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--divider" colspan="4" height="19"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr class="st-Divider">
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td bgcolor="#fdfdfd" colspan="2" height="1"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--body"
style="color: #000000 !important; border:0;margin:0;padding:0; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Ubuntu,sans-serif;font-size:16px;line-height:24px">
<h3>You have changed your password.</h3>
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--body"
style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
Your password has been updated. You can now log in.
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--body link-content"
style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
If you did not do this action, please send us an email at <a
style="color: #000000; text-decoration: none; font-weight: bold"
href="mailto:support@oneuptime.com" target="_blank">support@oneuptime.com</a> and let us
know.
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--body"
style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
Thanks, have a great day.
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--body"
style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
OneUptime Team.
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<table class="st-Footer st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--divider" colspan="3" height="20"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr class="st-Divider">
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td bgcolor="#fdfdfd" colspan="2" height="1"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--divider" colspan="3" height="31"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--caption"
style="border: 0; margin: 0;padding: 0; color: #8898aa; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; line-height: 16px;">
<span class="st-Delink st-Delink--footer"
style="border: 0; margin: 0; padding: 0; color: #8898aa; text-decoration: none;">
© {{year}} OneUptime Inc.
</span>
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--emailEnd" colspan="3" height="64"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!-- /Wrapper -->
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--emailEnd" height="64"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler">&nbsp;</div>
</td>
</tr>
</tbody>
</table>
<!-- /Background -->
</body>
</html>