Move login.ejs into a vite handled view

This commit is contained in:
Johannes Zellner
2024-12-13 22:29:34 +01:00
parent 0513ed16bb
commit d9402bc24d
5 changed files with 142 additions and 129 deletions

View File

@@ -481,20 +481,24 @@ function renderInteractionPage(provider) {
switch (prompt.name) {
case 'login': {
const options = {
submitUrl: `${ROUTE_PREFIX}/interaction/${uid}/login`,
iconUrl: '/api/v1/cloudron/avatar',
name: client?.name || await branding.getCloudronName(),
footer: marked.parse(await branding.renderFooter()),
note: (client.id === tokens.ID_WEBADMIN && constants.DEMO) ? '<div style="text-align: center;">This is a demo. Username and password is "cloudron"</div>' : ''
SUBMIT_URL: `${ROUTE_PREFIX}/interaction/${uid}/login`,
ICON_URL: '/api/v1/cloudron/avatar',
NAME: client?.name || await branding.getCloudronName(),
FOOTER: marked.parse(await branding.renderFooter()),
NOTE: (client.id === tokens.ID_WEBADMIN && constants.DEMO) ? '<div style="text-align: center;">This is a demo. Username and password is "cloudron"</div>' : ''
};
if (app) {
options.name = app.label || app.fqdn;
options.iconUrl = app.iconUrl;
options.NAME = app.label || app.fqdn;
options.ICON_URL = app.iconUrl;
}
const template = fs.readFileSync(__dirname + '/oidc_templates/login.ejs', 'utf-8');
const html = ejs.render(translations.translate(template, translationAssets), options);
// great ejs replacement!
const template = fs.readFileSync(__dirname + '/../dashboard/login.html', 'utf-8');
let html = translations.translate(template, translationAssets);
Object.keys(options).forEach(key => {
html = html.replace(`##${key}##`, options[key], 'g');
});
return res.send(html);
}

View File

@@ -1,255 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<title>{{ login.loginTo }} <%= name %></title>
<link id="favicon" type="image/png" rel="icon" href="<%= iconUrl %>">
<link rel="apple-touch-icon" href="<%= iconUrl %>">
<link rel="icon" href="<%= iconUrl %>">
<!-- Theme CSS -->
<link type="text/css" rel="stylesheet" href="/theme.css">
<style>
body {
background-image: url('/api/v1/cloudron/background');
background-size: cover;
background-position: center;
}
.card {
padding: 20px;
margin-bottom: 0;
max-width: 620px;
min-height: 100%;
}
.avatar {
margin-top: 20px;
}
@media(min-width:620px) {
.card {
margin-bottom: 15px;
margin-top: 100px;
min-height: auto;
}
.avatar {
margin-top: -84px
}
}
.hide {
display: none;
}
#busyIndicator {
display: inline-block;
width: 15px;
height: 15px;
border: 1px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
margin-right: 5px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.btn {
display: flex;
align-items: center;
}
h1 {
padding: 0;
margin-top: 10px;
text-overflow: ellipsis;
overflow-x: hidden;
line-height: 1.5;
}
small {
display: block;
margin-top: 25px;
color: #777777;
font-size: 20px;
font-family: "Noto Sans Light", Helvetica, Arial, sans-serif;
}
</style>
</head>
<body>
<div class="layout-root hide">
<div class="layout-content">
<div class="card">
<div class="row">
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" class="avatar" src="<%= iconUrl %>"/>
<br/>
<small>{{ login.loginTo }}</small>
<h1><%= name %></h1>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<%- note %>
<form id="loginForm">
<div class="form-group">
<label class="control-label" for="inputUsername">{{ login.username }}</label>
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">{{ login.password }}</label>
<input type="password" class="form-control" name="password" id="inputPassword" required password-reveal>
<p class="has-error hide" id="passwordError">{{ login.errorIncorrectCredentials }}</p>
<p class="has-error hide" id="internalError">{{ login.errorInternal }}</p>
</div>
<div class="form-group">
<label class="control-label" for="inputTotpToken">{{ login.2faToken }}</label>
<input type="text" class="form-control" name="totpToken" id="inputTotpToken" value="">
<p class="has-error hide" id="totpError">{{ login.errorIncorrect2FAToken }}</p>
</div>
<div class="card-form-bottom-bar">
<a href="/passwordreset.html">{{ login.resetPasswordAction }}</a>
<button class="btn btn-primary btn-outline" type="submit" id="loginSubmitButton"><div id="busyIndicator" class="hide"></div> {{ login.signInAction }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
<footer class="text-center">
<span class="text-muted"><%- footer %></span>
</footer>
</div>
<script>
window.addEventListener('load', function () {
var svgEye = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>';
var svgEyeSlash = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>';
document.querySelectorAll('[password-reveal]').forEach(function (element) {
var eye = document.createElement('i');
eye.innerHTML = svgEyeSlash;
eye.style.width = '18px';
eye.style.height = '18px';
eye.style.position = 'relative';
eye.style.float = 'right';
eye.style.marginTop = '-24px';
eye.style.marginRight = '10px';
eye.style.cursor = 'pointer';
eye.addEventListener('click', function () {
if (element.type === 'password') {
element.type = 'text';
eye.innerHTML = svgEye;
} else {
element.type = 'password';
eye.innerHTML = svgEyeSlash;
}
});
element.parentNode.style.position = 'relative';
element.parentNode.insertBefore(eye, element.nextSibling);
});
});
document.getElementById('loginForm').addEventListener('submit', function (event) {
event.preventDefault();
document.getElementById('passwordError').classList.add('hide');
document.getElementById('totpError').classList.add('hide');
document.getElementById('internalError').classList.add('hide');
document.getElementById('busyIndicator').classList.remove('hide');
const apiUrl = '<%= submitUrl %>';
const body = {
username: document.getElementById('inputUsername').value,
password: document.getElementById('inputPassword').value,
totpToken: document.getElementById('inputTotpToken').value
};
let res;
fetch(apiUrl, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-type': 'application/json; charset=UTF-8' }
}).then(function (response) {
res = response;
return res.json(); // we always return objects
}).then(function (data) {
if (res.status === 410) {
// the oidc login session is old
window.location.reload();
} else if (res.status === 401) {
if (data.message.indexOf('totpToken') !== -1) {
document.getElementById('inputTotpToken').value = '';
document.getElementById('inputTotpToken').focus();
document.getElementById('totpError').classList.remove('hide');
} else {
document.getElementById('inputPassword').value = '';
document.getElementById('inputPassword').focus();
document.getElementById('passwordError').classList.remove('hide');
}
document.getElementById('busyIndicator').classList.add('hide');
return;
} else if (res.status !== 200) {
document.getElementById('busyIndicator').classList.add('hide');
throw new Error('Something went wrong');
}
if (data.redirectTo) window.location.href = data.redirectTo;
else console.log('login success but missing redirectTo in data:', data);
}).catch(function (error) {
document.getElementById('internalError').classList.remove('hide');
document.getElementById('busyIndicator').classList.add('hide');
console.warn(error, res);
});
});
// placed in local storage by setupaccount.js
const autoLoginToken = localStorage.cloudronFirstTimeToken;
if (autoLoginToken) {
const apiUrl = '<%= submitUrl %>';
let res;
fetch(apiUrl, {
method: 'POST',
body: JSON.stringify({ autoLoginToken }),
headers: { 'Content-type': 'application/json; charset=UTF-8' }
}).then(function (res) {
localStorage.removeItem('cloudronFirstTimeToken');
return res.json(); // we always return objects
}).then(function (data) {
if (data.redirectTo) window.location.href = data.redirectTo;
else console.log('login success but missing redirectTo in data:', data);
}).catch(function (error) {
document.getElementsByClassName('layout-root')[0].classList.remove('hide');
localStorage.removeItem('cloudronFirstTimeToken');
document.getElementById('internalError').classList.remove('hide');
document.getElementById('busyIndicator').classList.add('hide');
console.warn(error, res);
});
} else {
document.getElementsByClassName('layout-root')[0].classList.remove('hide');
}
</script>
</body>
</html>