fleet/frontend/templates/enroll-ota.html
Magnus Jensen 9a859736c2
IdP Authentication before BYOD (#32017)
fixes: #29222 

This is a feature branch that was completed last week, but did not get
merged in time.

All pr's going in was approved, and reviewed.

I will after this is merged, do a cherry pick onto the RC 4.73 branch,
and initiate the FR merge process.

---------

Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com>
Co-authored-by: Gabriel Hernandez <ghernandez345@gmail.com>
2025-08-18 18:31:53 +02:00

441 lines
13 KiB
HTML

<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex" />
<link rel="shortcut icon" href="{{.URLPrefix}}/assets/favicon.ico" />
<title>Fleet</title>
<style>
@font-face {
font-family: "Inter";
font-weight: 400;
src: url("../assets/fonts/inter/Inter-Regular.woff2") format("woff2"),
url("../assets/fonts/inter/Inter-Regular.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-weight: 700;
src: url("../assets/fonts/inter/Inter-Bold.woff2") format("woff2"),
url("../assets/fonts/inter/Inter-Bold.woff") format("woff");
}
html {
box-sizing: border-box;
font-family: "Inter", sans-serif;
color: #192147;
}
*,
*:before,
*:after {
box-sizing: border-box;
}
body,
h1,
p {
margin: 0;
}
.download-link,
.enroll-link {
display: inline-block;
color: #fff;
background-color: #6a67fe;
padding: 8px 16px;
border-radius: 4px;
text-decoration: none;
font-weight: 700;
font-size: 14px;
line-height: 21px;
text-align: center;
}
header {
background-color: #192147;
padding: 13px 20px;
}
h1 {
margin-bottom: 8px;
}
p {
font-size: 14px;
}
ol {
display: flex;
flex-direction: column;
gap: 48px;
list-style: none;
padding: 0;
}
li {
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
.android-content {
margin-top: 48px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.page-description {
font-size: 16px;
margin-bottom: 48px;
}
.profile-downloaded-container,
.install-profile-container {
width: 100%;
background-color: #f9fafc;
text-align: center;
}
.profile-downloaded-img,
.install-profile-img {
max-width: 100%;
}
.profile-downloaded-img {
max-height: 150px;
}
.install-profile-img {
max-height: 118px;
}
#main-content {
margin: 0 auto;
padding: 48px 24px;
max-width: 800px;
}
.device-enroll-message {
margin-top: 48px;
display: flex;
gap: 8px;
}
.error-header {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
margin-bottom: 24px;
font-size: 14px;
}
.error-description {
text-align: center;
}
@media screen and (max-width: 430px) {
.enroll-link {
width: 100%;
}
}
</style>
</head>
<body>
<!-- unsupported platform content -->
<template id="unsupported-template">
<h1>Enroll your device in Fleet</h1>
<p class="page-description">
To enroll your iPhone, iPad, or Android device, please open this page on
your mobile device.
</p>
</template>
<!-- android content -->
<template id="android-template">
<h1>Enroll your Android device in Fleet</h1>
<div class="android-content">
<p>Select <b>Enroll</b> and follow the steps.</p>
<a class="enroll-link" href="" target="_blank">Enroll</a>
</div>
</template>
<!-- ios, ipados content -->
<template id="ios-ipad-template">
<h1>
Enroll your
<span data-attribute="dynamic-device-type">iPhone or iPad</span> in
Fleet
</h1>
<p class="page-description">
On your
<span data-attribute="dynamic-device-type">iPhone or iPad</span>, follow
the instructions below to download and install the Fleet profile.
</p>
<ol>
<li>
<p>
<span>1.</span>
<span>
<b>Download</b> the Fleet profile and select <b>Allow</b> in the
pop-up.
</span>
</p>
<a class="download-link" href="{{.EnrollURL}}">Download</a>
</li>
<li>
<p>
<span>2.</span>
<span>
Navigate to <b>Settings</b> and select <b>Profile Downloaded</b>.
</span>
</p>
<div class="profile-downloaded-container">
<img
class="profile-downloaded-img"
src=""
alt="select profile downloaded in settings"
/>
</div>
</li>
<li>
<p>
<span>3.</span>
<span>Select <b>Install</b>.</span>
</p>
<div class="install-profile-container">
<img class="install-profile-img" src="" alt="select install" />
</div>
<p>
You will see an UNVERIFIED PROFILE warning and a MOBILE DEVICE
MANAGEMENT warning. Click <b>Install</b> again for each warning.
When you see a <b>Remote Management</b> prompt, click
<b>Trust</b> to complete enrollment.
</p>
</li>
</ol>
<p class="device-enroll-message">
<img src="{{.URLPrefix}}/assets/images/check.svg" />
Your device will now be enrolled in Fleet.
</p>
</template>
<!-- Error content -->
<template id="error-template">
<h1 class="error-header">
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 0.5C3.58 0.5 0 4.08 0 8.5C0 12.92 3.58 16.5 8 16.5C12.42 16.5 16 12.92 16 8.5C16 4.08 12.42 0.5 8 0.5ZM8 3.75C8.41421 3.75 8.75 4.08579 8.75 4.5V9.5C8.75 9.91421 8.41421 10.25 8 10.25C7.58579 10.25 7.25 9.91421 7.25 9.5V4.5C7.25 4.08579 7.58579 3.75 8 3.75ZM8 13.5C8.55229 13.5 9 13.0523 9 12.5C9 11.9477 8.55229 11.5 8 11.5C7.44772 11.5 7 11.9477 7 12.5C7 13.0523 7.44772 13.5 8 13.5Z"
fill="#D66C7B"
/>
</svg>
<p class="error-title"></p>
</h1>
<p class="error-description"></p>
</template>
<header>
<img src="{{.URLPrefix}}/assets/images/fleet-logo.svg" />
</header>
<!-- container for the dynamic content -->
<section id="main-content"></section>
<script>
const ANDROID_GET_TOKEN_URL =
"{{.URLPrefix}}/api/v1/fleet/android_enterprise/enrollment_token";
// using string comparison to satisfy the linter.
const ANDROID_MDM_ENABLED = "{{.AndroidMDMEnabled}}" === "true";
const MAC_MDM_ENABLED = "{{.MacMDMEnabled}}" == "true";
const ERROR_MESSAGE = "{{.ErrorMessage}}";
const getPlatform = () => {
const userAgent = navigator.userAgent;
const isIPhone = /iPhone/i.test(userAgent);
const isIPad =
/iPad/i.test(userAgent) ||
(/Macintosh/i.test(userAgent) &&
navigator.maxTouchPoints !== undefined &&
navigator.maxTouchPoints > 1);
const isAndroid = /Android/i.test(userAgent);
if (isAndroid) {
return "android";
} else if (isIPhone) {
return "ios";
} else if (isIPad) {
return "ipad";
}
return "unsupported";
};
const getTemplateIdFromPlatform = (platform) => {
let templateId = "";
if (platform === "android") {
templateId = "android-template";
} else if (platform === "ios" || platform === "ipad") {
templateId = "ios-ipad-template";
} else {
templateId = "unsupported-template";
}
return templateId;
};
// renders the content based on the template id
const renderContent = (templateId) => {
const template = document.getElementById(templateId);
if (template) {
document
.getElementById("main-content")
.appendChild(template.content.cloneNode(true));
}
};
const setEnrollTokenUrl = (url) => {
document.querySelector(".enroll-link").setAttribute("href", url);
};
// dynmaic content rendering for ios and ipad only
const setIosIpadContent = (platform) => {
if (platform === "ios") {
deviceType = "iPhone";
} else if (platform === "ipad") {
deviceType = "iPad";
}
document
.querySelectorAll('[data-attribute="dynamic-device-type"]')
.forEach((el) => {
el.textContent = deviceType;
});
// update image src based on OS
const osImagePrefix = platform === "ios" ? "ios" : "iPadOS";
document.querySelector(
".profile-downloaded-img"
).src = `{{.URLPrefix}}/assets/images/${osImagePrefix}-profile-downloaded.png`;
document.querySelector(
".install-profile-img"
).src = `{{.URLPrefix}}/assets/images/${osImagePrefix}-install-profile.png`;
};
const renderError = (title, description) => {
const template = document.getElementById("error-template");
if (template) {
const clone = template.content.cloneNode(true);
clone.querySelector(".error-title").textContent = title;
clone.querySelector(".error-description").textContent = description;
document.getElementById("main-content").appendChild(clone);
}
};
const getEnrollmentSecret = () => {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get("enroll_secret");
};
const getEnrollmentToken = async () => {
const enrollSecret = getEnrollmentSecret();
const response = await fetch(
`${ANDROID_GET_TOKEN_URL}?enroll_secret=${encodeURIComponent(
enrollSecret
)}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error("Failed to get enrollment token");
}
return await response.json();
};
// here we render the page content
document.addEventListener("DOMContentLoaded", async function () {
const platform = getPlatform();
// quick exit if platform is unsupported
if (platform === "unsupported") {
renderContent("unsupported-template");
return;
}
// if there is an error message, render it
if (ERROR_MESSAGE) {
// format of error message is expected to use colon as separator character, e.g., "An error occurred. : Failed to parse original URL."
const [title, description] = ERROR_MESSAGE.split(":");
renderError(title.trim(), description.trim());
return;
}
// check if the enroll secret is valid.
if (!getEnrollmentSecret()) {
renderError(
"This URL is invalid.",
"Enroll secret is invalid. Please contact your IT admin."
);
return;
}
let templateId = getTemplateIdFromPlatform(platform);
// handle android rendering
if (platform === "android") {
if (!ANDROID_MDM_ENABLED) {
renderError(
"Android MDM is turned off.",
"To enroll your Android device please contact your IT admin."
);
return;
}
// we need to get the token and then place it in the enroll link
try {
const data = await getEnrollmentToken();
renderContent(templateId);
if (!data.android_enrollment_url) {
throw new Error("Failed to get enrollment token");
}
setEnrollTokenUrl(data.android_enrollment_url);
} catch (error) {
renderError(
"This URL is invalid.",
"Enroll secret is invalid. Please contact your IT admin."
);
return;
}
}
// handle rendering for ios and ipad
if (platform === "ios" || platform === "ipad") {
if (!MAC_MDM_ENABLED) {
const description = `To enroll your ${
platform === "ios" ? "iPhone" : "iPad"
} please contact your IT admin.`;
renderError("Apple MDM is turned off.", description);
return;
}
renderContent(templateId);
setIosIpadContent(platform);
}
});
</script>
</body>
</html>