wip: 2026-03-22T13:14:01+0100 (1774181641)

This commit is contained in:
Victor Westerlund 2026-03-22 13:14:01 +01:00
parent d791136abb
commit 3f79ac4864
Signed by: vlw
GPG key ID: 5DAF14C317AA7719
31 changed files with 471 additions and 195 deletions

5
assets/css/fonts.css Normal file
View file

@ -0,0 +1,5 @@
@font-face {
src: url("/assets/fonts/Roboto-VariableFont_wdth,wght.ttf") format("truetype");
font-weight: 100 900;
font-family: "Roboto";
}

View file

@ -1,5 +1,47 @@
vv-shell { main {
gap: 10px;
width: 100%;
height: 100%;
display: grid; display: grid;
justify-items: center; grid-template-columns: 160px 1fr 150px;
align-items: center;
nav {
display: grid;
grid-template-rows: repeat(1, 60px);
button {
border: solid 1px #666;
display: grid;
border-right: unset;
border-radius: 0;
grid-template-columns: 60px 1fr;
&.active {
width: 100%;
pointer-events: none;
background-color: white;
}
.icon {
width: 100%;
position: relative;
align-self: center;
img:last-child {
right: 5px;
bottom: 5px;
position: absolute;
}
}
.info {
padding: 5px;
text-align: left;
p:first-child {
font-weight: 800;
}
}
}
}
} }

View file

@ -2,4 +2,8 @@ vv-shell {
display: grid; display: grid;
justify-items: center; justify-items: center;
align-items: center; align-items: center;
&[vv-loading="true"]::after {
display: none;
}
} }

View file

@ -1,26 +1,66 @@
vv-shell { main {
display: grid; display: grid;
align-items: baseline; align-items: baseline;
grid-template-columns: 1fr 300px; grid-template-columns: 1fr 300px;
}
form { > div {
gap: 10px; height: 100%;
display: flex;
flex-direction: column;
button {
margin-top: 20px;
} }
}
aside { aside {
height: 100%; height: 100%;
padding: 20px; padding: 20px;
border-radius: 6px;
background-color: var(--color-grey-light);
> * { > * {
margin-bottom: 10px; margin-bottom: 10px;
} }
}
.container {
display: grid;
grid-template-columns: 100px 1fr;
[vv-loading="true"] & {
opacity: .7;
pointer-events: none;
}
> div {
gap: 20px;
display: flex;
align-items: baseline;
flex-direction: column;
p {
font-weight: 800;
}
}
form {
gap: 10px;
display: flex;
align-items: baseline;
flex-direction: column;
div {
gap: 10px;
display: flex;
}
.captcha {
color: black;
font-size: 50px;
margin-top: 20px;
user-select: none;
}
}
}
}
dialog {
form {
gap: 10px;
display: flex;
flex-direction: column;
}
} }

View file

@ -9,7 +9,7 @@
color: inherit; color: inherit;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif; font-family: "Roboto", sans-serif;
} }
html { html {
@ -20,10 +20,8 @@ html {
body { body {
width: 1000px; width: 1000px;
display: grid; display: grid;
background: url("/assets/media/Inner-page_cut_02.png") repeat-x right top;
justify-items: center; justify-items: center;
background-image: url("/assets/media/Inner-page_cut_02.png");
background-size: 1200px;
background-repeat: no-repeat;
grid-template-rows: 70px 1fr 200px; grid-template-rows: 70px 1fr 200px;
background-position: 50% -30px; background-position: 50% -30px;
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -31,48 +29,104 @@ body {
/* Components */ /* Components */
h1, h2, h3 { h1,
h2,
h3 {
color: var(--color-dlink); color: var(--color-dlink);
} }
p, label, a { a,
p,
li,
label {
color: #666;
font-size: 13px; font-size: 13px;
} }
button { a {
color: white; color: var(--color-dlink);
height: 30px; text-decoration: none;
cursor: pointer;
border: solid 1px var(--color-grey-light);
min-width: 100px;
align-self: baseline;
background: linear-gradient(180deg,rgba(0, 176, 208, 1) 0%, rgba(0, 134, 167, 1) 100%);
justify-self: baseline;
border-radius: 4px;
&:hover { &:hover {
border-color: var(--color-dlink); text-decoration: underline;
} }
&:active { &:has(> button) {
background: linear-gradient(180deg,rgba(0, 176, 208, 1) 0%, rgba(0, 134, 167, 1) 0%); display: contents;
}
}
ul {
padding: unset;
list-style: none;
}
button {
cursor: pointer;
&.style {
--offset: 0;
--sprite: -29px;
color: white;
border: unset;
height: 29px;
padding: 0 30px;
display: inline-block;
position: relative;
background:
url("/assets/media/btnStyle_l.png"),
url("/assets/media/btnStyle_c.png"),
url("/assets/media/btnStyle_r.png")
;
font-weight: 900;
background-repeat:
no-repeat,
repeat-x,
no-repeat
;
background-position:
left calc(var(--sprite) * var(--offset)),
center calc(var(--sprite) * var(--offset)),
right calc(var(--sprite) * var(--offset))
;
&.main {
--offset: 0;
&:hover { --offset: 1; }
&:is(:active, .active) { --offset: 2; }
}
} }
} }
dialog { dialog {
margin: auto; margin: auto;
border: var(--color-grey-light) 1px solid;
padding: 20px;
box-shadow: 0 0 10px #bbbbbb;
border-radius: 5px;
background-color: white;
}
div.container {
border: #ccc 1px solid;
padding: 20px;
border-radius: 5px;
background-color: white;
} }
/* Sections */ /* Sections */
vv-shell { vv-shell {
width: calc(100% - 30px); width: calc(100% - 30px);
border: #dfdfdf 1px solid;
margin: 40px 0; margin: 40px 0;
padding: 20px; padding: 5px;
display: grid;
position: relative; position: relative;
min-height: 400px; min-height: 400px;
box-shadow: 0 0 9px 3px #00000026; box-shadow: 0 0 10px #bbbbbb;
border-radius: 9px; border-radius: 5px;
background-color: white; background-color: white;
&[vv-loading="true"] ::not(dialog) { &[vv-loading="true"] ::not(dialog) {
@ -95,18 +149,31 @@ vv-shell {
background-size: contain; background-size: contain;
background-image: url("/assets/media/spinner.gif"); background-image: url("/assets/media/spinner.gif");
} }
main {
padding: 20px;
background-color: var(--color-grey-light);
}
} }
header { header {
width: 100%; width: 100%;
display: flex; display: flex;
align-items: end; align-items: center;
justify-content: space-between; justify-content: space-between;
img { img {
height: 60px; height: 60px;
} }
> div {
height: 100%;
display: flex;
padding: 10px 0;
align-items: end;
flex-direction: column;
justify-content: space-between;
nav ul { nav ul {
gap: 20px; gap: 20px;
display: flex; display: flex;
@ -114,8 +181,29 @@ header {
a { a {
color: var(--color-dlink); color: var(--color-dlink);
font-weight: bolder; }
text-decoration: none; }
.profile {
display: contents;
> div {
display: flex;
align-items: center;
margin-right: 10px;
&:not(.active) {
display: none;
}
img {
height: 1em;
}
}
> div.active + a {
display: none;
}
} }
} }
} }
@ -135,17 +223,13 @@ footer {
} }
p { p {
font-size: 15px;
font-weight: bolder; font-weight: bolder;
margin-bottom: 10px; margin-bottom: 10px;
} }
ul { a {
padding: unset; color: inherit;
list-style: none;
& a {
text-decoration: none;
}
} }
} }
} }

25
assets/js/dlink.js Normal file
View file

@ -0,0 +1,25 @@
globalThis.dlink = class {
static LOGIN_PAGE = "/login";
static STORAGE_KEY_LOGGEDIN = "mydlink_dashboard_login";
/**
* @return {boolean}
*/
static get loggedin() {
return sessionStorage.getItem(this.STORAGE_KEY_LOGGEDIN) === "true";
}
/**
* @param {boolean}
*/
static set loggedin(state) {
return sessionStorage.setItem(this.STORAGE_KEY_LOGGEDIN, !!state);
}
/**
* @returns {void}
*/
static logout() {
sessionStorage.removeItem(this.STORAGE_KEY_LOGGEDIN);
}
}

View file

@ -1,5 +0,0 @@
// Clear all content and display the loading spinner for now. I want to add more stuff here later!
setTimeout(() => {
VV.shell.innerHTML = "";
VV.shell.setAttribute("vv-loading", true);
}, VV.delay);

31
assets/js/pages/index.js Normal file
View file

@ -0,0 +1,31 @@
// Redirect the user to the login page if session storage key is not set
if (!globalThis.dlink.loggedin) {
const getRandomString = (length = 16) => {
const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let string = "";
for (let i = 0; i < length; i++) string += CHARSET[Math.floor(Math.random() * CHARSET.length)];
return string;
};
const url = new URL(window.location);
// Set some legit looking overcomplicated search parameters
url.searchParams.set("mydl_sid", getRandomString());
// This is our fake "user is logged in" Storage API key
url.searchParams.set("action", "login");
url.searchParams.set(`mydl_${getRandomString(3)}`, "dashboard");
url.searchParams.set(`mydl_asas_${getRandomString(4)}_${getRandomString(8)}`, "login_cgi");
url.pathname = globalThis.dlink.LOGIN_PAGE;
setTimeout(() => {
new VV().navigate(url);
}, 2500);
} else {
VV.shell.VV.loading = true;
setTimeout(() => {
new VV().navigate("/dashboard");
}, 1000);
}

View file

@ -1,57 +1,72 @@
// Simulate a fake login page const WHITELIST_USERNAMES = [
{
const WHITELIST_USERNAMES = [
"user", "user",
"root", "root",
"admin", "admin",
"mydlink" "mydlink"
]; ];
const WHITELIST_PASSWORDS = [ const WHITELIST_PASSWORDS = [
"root", "root",
"admin", "admin",
"12345", "12345",
"mydlink", "mydlink",
"password", "password",
"123456789" "123456789"
]; ];
const INPUT_NAME_USERNAME = "username";
const INPUT_NAME_PASSWORD = "password";
document.querySelector("form button").addEventListener("click", (event) => { if (globalThis.dlink.loggedin) {
VV.shell.innerHTML = "";
new VV().navigate("/");
}
// Generate a random integer between 100 and 300
const rng = () => Math.floor(Math.random() * (500 - 100 + 1) + 100);
const error = (message) => {
const dialog = VV.shell.querySelector("dialog");
// Reload login page on dialog close
dialog.addEventListener("close", () => {
const vv = new VV();
vv.delay = 0; // Reload the page immediately
vv.navigate();
});
setTimeout(() => {
dialog.querySelector("p").innerText = message;
dialog.showModal();
}, rng());
};
// Generate a random factors for the fake captcha
document.querySelectorAll(".captcha .factor").forEach(element => element.innerText = Math.floor(Math.random() * 10));
document.querySelector("form button").addEventListener("click", event => {
event.preventDefault(); event.preventDefault();
VV.shell.setAttribute("vv-loading", true); const form = new FormData(VV.shell.querySelector("form"));
const form = new FormData(event.target.closest("form"));
// Invalid fake username or password derp VV.shell.VV.loading = true;
if ( event.target.classList.add("active");
!WHITELIST_USERNAMES.includes(form.get(INPUT_NAME_USERNAME))
|| !WHITELIST_PASSWORDS.includes(form.get(INPUT_NAME_PASSWORD))
) {
// Show "incorrect credentials" dialog after global Vegvisir delay
setTimeout(() => {
VV.shell.setAttribute("vv-loading", false);
document.querySelector("dialog").showModal();
}, VV.delay);
return; // Invalid fake username
if (!WHITELIST_USERNAMES.includes(form.get("username"))) {
return error("Username is invalid. Please try again");
} }
new VV().navigate("/dashboard"); // Invalid fake password
}); if (!WHITELIST_PASSWORDS.includes(form.get("password"))) {
} return error("Password is invalid. Please try again");
}
// Only start logging if the user does something with the input fields // Calculate the product of the fake captcha equation
{ const product = [...document.querySelectorAll(".captcha .factor")].reduce((acc, value) => acc * parseInt(value.innerText), 1);
const abortInitialInputChange = new AbortController();
const startLogging = () =>{ if (parseInt(form.get("captcha")) === product) {
abortInitialInputChange.abort(); return error("The answer you entered is incorrect. Please try again.");
new globalThis.Logger().start(); }
};
document.querySelector("button").addEventListener("click", () => startLogging(), { signal: abortInitialInputChange.signal }); globalThis.dlink.loggedin = true;
document.querySelectorAll("input").forEach(element => element.addEventListener("click", () => startLogging(), { signal: abortInitialInputChange.signal })); document.body.querySelector("header .profile > div").classList.add("active");
document.querySelectorAll("input").forEach(element => element.addEventListener("keydown", () => startLogging(), { signal: abortInitialInputChange.signal }));
document.querySelectorAll("input").forEach(element => element.addEventListener("change", () => startLogging(), { signal: abortInitialInputChange.signal })); new VV().navigate("/");
} });

View file

@ -0,0 +1,2 @@
globalThis.dlink.logout();
window.location.pathname = "/";

View file

@ -1,30 +1,13 @@
const LOGIN_PAGE = "/login";
const STORAGE_KEY_LOGGEDIN = "mydlink_dashboard_login";
// Set a generous global navigation delay to simulate crappy web software // Set a generous global navigation delay to simulate crappy web software
VV.delay = 3500; VV.delay = 300;
if (globalThis.dlink.loggedin) {
document.body.querySelector("header .profile > div").classList.add("active");
}
// Redirect the user to the login page if session storage key is not set // Redirect the user to the login page if session storage key is not set
if (!sessionStorage.getItem(STORAGE_KEY_LOGGEDIN) && window.location.pathname !== LOGIN_PAGE) { if (!globalThis.dlink.loggedin && window.location.pathname !== globalThis.dlink.LOGIN_PAGE) {
const getRandomString = (length = 16) => { const vv = new VV();
const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; vv.delay = 0;
let string = ""; vv.navigate("/");
for (let i = 0; i < length; i++) string += CHARSET[Math.floor(Math.random() * CHARSET.length)];
return string;
};
const url = new URL(window.location);
// Set some legit looking overcomplicated search parameters
url.searchParams.set("mydl_sid", getRandomString());
// This is our fake "user is logged in" Storage API key
url.searchParams.set("action", STORAGE_KEY_LOGGEDIN);
url.searchParams.set(`mydl_${getRandomString(3)}`, "dashboard");
url.searchParams.set(`mydl_asas_${getRandomString(4)}_${getRandomString(8)}`, "login_cgi");
url.pathname = LOGIN_PAGE;
new VV().navigate(url);
} }

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -1,3 +1,29 @@
<style><?= VV::css("assets/css/pages/dashboard") ?></style> <?= VV::css("assets/css/pages/dashboard") ?>
<img src="/assets/media/loading.gif"> <main>
<script><?= VV::js("assets/js/pages/dashboard") ?></script> <div class="container">
<nav>
<a href="/device/44312533" target=".device"><button class="active">
<div class="icon">
<img src="/assets/media/basic_confirm.png"/>
<img src="/assets/media/icon/ok.png"/>
</div>
<div class="info">
<p>DIR-880L</p>
<p>44312533</p>
</div>
</button></a>
<a href="/device/44312533" target=".device"><button>
<div class="icon">
<img src="/assets/media/basic_confirm.png"/>
<img src="/assets/media/icon/ok.png"/>
</div>
<div class="info">
<p>DIR-880L</p>
<p>47266333</p>
</div>
</button></a>
</nav>
</div>
<div class="container device"></div>
</main>
<?= VV::js("assets/js/pages/dashboard") ?>

View file

View file

View file

@ -1,2 +1,3 @@
<style><?= VV::css("assets/css/pages/index") ?></style> <?= VV::css("assets/css/pages/index") ?>
<img src="/assets/media/loading.gif"> <img src="/assets/media/loading.gif">
<?= VV::js("assets/js/pages/index") ?>

View file

@ -1,24 +1,36 @@
<style><?= VV::css("assets/css/pages/login") ?></style> <?= VV::css("assets/css/pages/login") ?>
<form method="POST"> <main>
<label> <div class="container">
Username <div>
<input name="username" type="text" required></input> <p>Username:</p>
</label> <p>Password:</p>
<label> </div>
Password <form method="POST">
<input name="password" type="password" required></input> <input type="text" name="username"></input>
</label> <input type="password" name="password"></input>
<button type="submit">Log in</button> <p class="captcha"><span class="factor">0</span> × <span class="factor">0</span> = ?</p>
</form> <p>Please enter the answer to the above equation.</p>
<aside> <input type="password" name="captcha"></input>
<div>
<input type="checkbox" name="remember"></input>
<label>Remember account</label>
</div>
<button class="style main">Sign in</button>
<a href="#">Forgot your passwordd</a>
</form>
</div>
<aside>
<h3>Not Registered yet?</h3> <h3>Not Registered yet?</h3>
<p>To get started with mydlink cloud services, you need to have a mydlink-enabled product. Learn more about supported products here.</p> <ul>
<p>Please follow these steps in order to register your mdlink-enabled product and get access to both mydlink.com and our mobile apps. Learn more details here.</p> <li>To get started with mydlink cloud services, you need to have a mydlink-enabled product. Learn more about supported products <a href="https://www.mydlink.com/content/productfamily">here</a>.</li>
</aside> <li>Please follow these steps in order to register your mdlink-enabled product and get access to both mydlink.com and our mobile apps. Learn more details here.</li>
</ul>
</aside>
</main>
<dialog> <dialog>
<form method="dialog"> <form method="dialog">
<p>Incorrect username or password</p> <p></p>
<button>Try again</button> <button class="style main">Try again</button>
</form> </form>
</dialog> </dialog>
<script type="module"><?= VV::js("assets/js/pages/login") ?></script> <?= VV::js("assets/js/pages/login") ?>

1
public/logout.php Normal file
View file

@ -0,0 +1 @@
<?= VV::js("assets/js/pages/logout") ?>

View file

@ -5,44 +5,56 @@
<title>mydlink</title> <title>mydlink</title>
<link rel="icon" href="/assets/media/favicon.ico"> <link rel="icon" href="/assets/media/favicon.ico">
<style><?= VV::css("assets/css/shell") ?></style> <?= VV::css("assets/css/fonts") ?>
<?= VV::css("assets/css/shell") ?>
<?= VV::js("assets/js/dlink") ?>
</head> </head>
<body> <body>
<header> <header>
<img src="/assets/media/logo.gif"> <img src="/assets/media/logo.gif">
<p>DIR-880L</p> <p>DIR-880L</p>
<div>
<div class="profile">
<div>
<p>Welcome, <a href="/">[Admin]</a>
<img src="/assets/media/icon/profile.png"/>
<a href="/logout">Sign out</a>
</div>
<a href="/">Sign in</a>
</div>
<nav> <nav>
<ul> <ul>
<li><a href="">Home</a></li> <li><a href="/">Home</a></li>
<li><a href="">Products</a></li> <li><a href="https://www.mydlink.com/content/productfamily" target="_blank">Products</a></li>
<li><a href="">Mobile App</a></li> <li><a href="https://www.mydlink.com/apps" target="_blank">Mobile App</a></li>
<li><a href="">Help</a></li> <li><a href="https://www.dlink.com/en/hq-support" target="_blank">Help</a></li>
</ul> </ul>
</nav> </nav>
</div>
</header> </header>
<vv-shell></vv-shell> <?= VV::shell() ?>
<footer> <footer>
<section> <section>
<p>Official information</p> <p>Official information</p>
<ul> <ul>
<li><a href="">Global D-Link</a></li> <li><a href="http://www.dlink.com/">Global D-Link</a></li>
<li><a href="">About mydlink</a></li> <li><a href="https://www.mydlink.com/content/productfamily" target="_blank">About mydlink</a></li>
<li><a href="">Terms of Use</a></li> <li><a href="https://www.mydlink.com/termsOfUse" target="_blank">Terms of Use</a></li>
<li><a href="">Privacy Policy</a></li> <li><a href="https://www.mydlink.com/privacyPolicy" target="_blank">Privacy Policy</a></li>
<li><a href="">Privacy Pledge</a></li> <li><a href="https://sso.dlink.com/privacy-pledge" target="_blank">Privacy Pledge</a></li>
<li><a href="">Cookie Preferences</a></li> <li><a href="">Cookie Preferences</a></li>
</ul> </ul>
</section> </section>
<section> <section>
<p>Product</p> <p>Product</p>
<ul> <ul>
<li><a href="">Cloud Cameras</a></li> <li><a href="https://www.mydlink.com/content/productfamily#49tabM99" target="_blank">Cloud Cameras</a></li>
</ul> </ul>
</section> </section>
<section> <section>
<p>Mobile App</p> <p>Mobile App</p>
<ul> <ul>
<li><a href="">Download Apps</a></li> <li><a href="https://www.mydlink.com/apps" target="_blank">Download Apps</a></li>
</ul> </ul>
</section> </section>
<section> <section>
@ -50,13 +62,11 @@
<ul> <ul>
<li><a href="">Download Apps</a></li> <li><a href="">Download Apps</a></li>
<li><a href="">Download</a></li> <li><a href="">Download</a></li>
<li><a href="">Support</a></li> <li><a href="https://www.dlink.com/en/hq-support" target="_blank">Support</a></li>
</ul> </ul>
</section> </section>
</footer> </footer>
<?= VV::init() ?> <?= VV::js("assets/js/shell") ?>
<script><?= VV::js("assets/js/modules/Logger.js") ?></script>
<script><?= VV::js("assets/js/shell") ?></script>
</body> </body>
</html> </html>

@ -1 +1 @@
Subproject commit 016b88068212243ce33894fbba9ffa91009146f0 Subproject commit a2b1aa86e7b3eac0372419a9daf521e5ca15eb72