Move login.ejs into a vite handled view
This commit is contained in:
22
src/oidc.js
22
src/oidc.js
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user