mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Add BNB_TOKEN_CLEANUP_INTERVAL variable and re-design login page
This commit is contained in:
@@ -107,7 +107,8 @@ const app = server(
|
||||
version,
|
||||
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
|
||||
externalImageResolver: artistImageFetcher,
|
||||
smapiTokenStore
|
||||
smapiTokenStore,
|
||||
tokenCleanupIntervalMinutes: config.tokenStore.cleanupIntervalMinutes
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ export default function () {
|
||||
bnbEnvVar<boolean>("REPORT_NOW_PLAYING", { default: true, parser: asBoolean }),
|
||||
tokenStore: {
|
||||
dbPath: bnbEnvVar<string>("TOKEN_DB_PATH", { default: "/config/tokens.db" })!,
|
||||
cleanupIntervalMinutes: bnbEnvVar<number>("TOKEN_CLEANUP_INTERVAL", { default: 60, parser: asInt })!,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ export type ServerOpts = {
|
||||
smapiAuthTokens: SmapiAuthTokens;
|
||||
externalImageResolver: ImageFetcher;
|
||||
smapiTokenStore: SmapiTokenStore;
|
||||
tokenCleanupIntervalMinutes: number;
|
||||
};
|
||||
|
||||
const DEFAULT_SERVER_OPTS: ServerOpts = {
|
||||
@@ -130,6 +131,7 @@ const DEFAULT_SERVER_OPTS: ServerOpts = {
|
||||
),
|
||||
externalImageResolver: axiosImageFetcher,
|
||||
smapiTokenStore: new InMemorySmapiTokenStore(),
|
||||
tokenCleanupIntervalMinutes: 60,
|
||||
};
|
||||
|
||||
function server(
|
||||
@@ -747,7 +749,8 @@ function server(
|
||||
i8n,
|
||||
serverOpts.smapiAuthTokens,
|
||||
serverOpts.smapiTokenStore,
|
||||
serverOpts.logRequests
|
||||
serverOpts.logRequests,
|
||||
serverOpts.tokenCleanupIntervalMinutes
|
||||
);
|
||||
|
||||
if (serverOpts.applyContextPath) {
|
||||
|
||||
@@ -406,7 +406,8 @@ function bindSmapiSoapServiceToExpress(
|
||||
i8n: I8N,
|
||||
smapiAuthTokens: SmapiAuthTokens,
|
||||
tokenStore: SmapiTokenStore,
|
||||
_logRequests: boolean
|
||||
_logRequests: boolean,
|
||||
tokenCleanupIntervalMinutes: number = 60
|
||||
) {
|
||||
const sonosSoap = new SonosSoap(bonobUrl, linkCodes, smapiAuthTokens, clock, tokenStore);
|
||||
|
||||
@@ -420,14 +421,16 @@ function bindSmapiSoapServiceToExpress(
|
||||
logger.error("Failed to cleanup expired tokens on startup", { error });
|
||||
}
|
||||
|
||||
// Clean up expired tokens every hour
|
||||
// Clean up expired tokens periodically
|
||||
const cleanupIntervalMs = tokenCleanupIntervalMinutes * 60 * 1000;
|
||||
logger.info(`Token cleanup will run every ${tokenCleanupIntervalMinutes} minute(s)`);
|
||||
setInterval(() => {
|
||||
try {
|
||||
tokenStore.cleanupExpired(smapiAuthTokens);
|
||||
} catch (error) {
|
||||
logger.error("Failed to cleanup expired tokens", { error });
|
||||
}
|
||||
}, 60 * 60 * 1000).unref(); // Run every hour, but don't prevent process exit
|
||||
}, cleanupIntervalMs).unref(); // Don't prevent process exit
|
||||
|
||||
const urlWithToken = (accessToken: string) =>
|
||||
bonobUrl.append({
|
||||
|
||||
@@ -240,7 +240,7 @@ describe("server", () => {
|
||||
.send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<h2>${lang("devices")} \(0\)</h2>`);
|
||||
expect(res.text).toMatch(new RegExp(`${lang("devices")}.*\\(0\\)`));
|
||||
expect(res.text).not.toMatch(/class=device/);
|
||||
expect(res.text).toContain(lang("noSonosDevices"));
|
||||
});
|
||||
@@ -276,7 +276,7 @@ describe("server", () => {
|
||||
.send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<h2>${lang("devices")} \(0\)</h2>`);
|
||||
expect(res.text).toMatch(new RegExp(`${lang("devices")}.*\\(0\\)`));
|
||||
expect(res.text).not.toMatch(/class=device/);
|
||||
expect(res.text).toContain(lang("noSonosDevices"));
|
||||
});
|
||||
@@ -290,7 +290,7 @@ describe("server", () => {
|
||||
.send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<h2>${lang("services")} \(0\)</h2>`);
|
||||
expect(res.text).toMatch(new RegExp(`${lang("services")}.*\\(0\\)`));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -352,9 +352,9 @@ describe("server", () => {
|
||||
.send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<h2>${lang("devices")} \(2\)</h2>`);
|
||||
expect(res.text).toMatch(/device1\s+\(172.0.0.1:4301\)/);
|
||||
expect(res.text).toMatch(/device2\s+\(172.0.0.2:4302\)/);
|
||||
expect(res.text).toMatch(new RegExp(`${lang("devices")}.*\\(2\\)`));
|
||||
expect(res.text).toMatch(/device1.*172\.0\.0\.1:4301/);
|
||||
expect(res.text).toMatch(/device2.*172\.0\.0\.2:4302/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -366,11 +366,11 @@ describe("server", () => {
|
||||
.send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<h2>${lang("services")} \(4\)</h2>`);
|
||||
expect(res.text).toMatch(/s1\s+\(1\)/);
|
||||
expect(res.text).toMatch(/s2\s+\(2\)/);
|
||||
expect(res.text).toMatch(/s3\s+\(3\)/);
|
||||
expect(res.text).toMatch(/s4\s+\(4\)/);
|
||||
expect(res.text).toMatch(new RegExp(`${lang("services")}.*\\(4\\)`));
|
||||
expect(res.text).toMatch(/s1.*SID:\s*1/);
|
||||
expect(res.text).toMatch(/s2.*SID:\s*2/);
|
||||
expect(res.text).toMatch(/s3.*SID:\s*3/);
|
||||
expect(res.text).toMatch(/s4.*SID:\s*4/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -382,14 +382,11 @@ describe("server", () => {
|
||||
.send();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(
|
||||
`<input type="submit" value="${lang("register")}">`
|
||||
);
|
||||
expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`);
|
||||
expect(res.text).toMatch(
|
||||
`<h3>${lang("noExistingServiceRegistration")}</h3>`
|
||||
`<input type="submit" value="${lang("register")}" id="submit">`
|
||||
);
|
||||
expect(res.text).toContain(lang("noExistingServiceRegistration"));
|
||||
expect(res.text).not.toMatch(
|
||||
`<input type="submit" value="${lang("removeRegistration")}">`
|
||||
`value="${lang("removeRegistration")}"`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -440,14 +437,11 @@ describe("server", () => {
|
||||
.send();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(
|
||||
`<input type="submit" value="${lang("register")}">`
|
||||
`<input type="submit" value="${lang("register")}" id="submit">`
|
||||
);
|
||||
expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`);
|
||||
expect(res.text).toContain(lang("existingServiceConfig"));
|
||||
expect(res.text).toMatch(
|
||||
`<h3>${lang("existingServiceConfig")}</h3>`
|
||||
);
|
||||
expect(res.text).toMatch(
|
||||
`<input type="submit" value="${lang("removeRegistration")}">`
|
||||
`<input type="submit" value="${lang("removeRegistration")}" id="submit"`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -632,13 +626,13 @@ describe("server", () => {
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<title>${lang("login")}</title>`);
|
||||
expect(res.text).toMatch(
|
||||
`<h1 class="login one-word-per-line">${lang("logInToBonob")}</h1>`
|
||||
`<h1>${lang("logInToBonob")}</h1>`
|
||||
);
|
||||
expect(res.text).toMatch(
|
||||
`<label for="username">${lang("username")}:</label>`
|
||||
`<label for="username">${lang("username")}</label>`
|
||||
);
|
||||
expect(res.text).toMatch(
|
||||
`<label for="password">${lang("password")}:</label>`
|
||||
`<label for="password">${lang("password")}</label>`
|
||||
);
|
||||
expect(res.text).toMatch(
|
||||
`<input type="submit" value="${lang("login")}" id="submit">`
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<% layout('./layout', { title: it.lang("failure") }) %>
|
||||
|
||||
<div id="content">
|
||||
<h1 class="failure"><%= it.message %></h1>
|
||||
<h1 class="cause"><%= it.cause || "" %></h1>
|
||||
<div class="message-container">
|
||||
<div class="logo">✗</div>
|
||||
<h1><%= it.message %></h1>
|
||||
<% if (it.cause) { %>
|
||||
<p style="color: #dc3545; margin-top: 15px;"><%= it.cause %></p>
|
||||
<% } %>
|
||||
<p style="color: #dc3545; font-weight: 600; margin-top: 10px;"><%= it.lang("failure") %></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,45 +1,61 @@
|
||||
<% layout('./layout') %>
|
||||
|
||||
<div id="content">
|
||||
<div width="100%" style="text-align:right;color:grey"><%= it.version %></div>
|
||||
<h1><%= it.bonobService.name %> (<%= it.bonobService.sid %>)</h1>
|
||||
<h3><%= it.lang("expectedConfig") %></h3>
|
||||
<div><%= JSON.stringify(it.bonobService) %></div>
|
||||
<br/>
|
||||
<div id="content" class="index-content">
|
||||
<div style="text-align:right;color:#999;font-size:0.85rem;margin-bottom:20px"><%= it.version %></div>
|
||||
<div class="logo">🎵</div>
|
||||
<h1><%= it.bonobService.name %></h1>
|
||||
<p style="color:#999;margin-bottom:30px">Service ID: <%= it.bonobService.sid %></p>
|
||||
|
||||
<% if(it.devices.length > 0) { %>
|
||||
<form action="<%= it.createRegistrationRoute %>" method="POST">
|
||||
<input type="submit" value="<%= it.lang("register") %>">
|
||||
<form action="<%= it.createRegistrationRoute %>" method="POST" style="margin-bottom:30px">
|
||||
<input type="submit" value="<%= it.lang("register") %>" id="submit">
|
||||
</form>
|
||||
<br/>
|
||||
<% } else { %>
|
||||
<h3><%= it.lang("noSonosDevices") %></h3>
|
||||
<br/>
|
||||
<p style="color:#dc3545;font-weight:600;margin:30px 0"><%= it.lang("noSonosDevices") %></p>
|
||||
<% } %>
|
||||
|
||||
<% if(it.registeredBonobService) { %>
|
||||
<h3><%= it.lang("existingServiceConfig") %></h3>
|
||||
<div><%= JSON.stringify(it.registeredBonobService) %></div>
|
||||
<% } else { %>
|
||||
<h3><%= it.lang("noExistingServiceRegistration") %></h3>
|
||||
<% } %>
|
||||
<% if(it.registeredBonobService) { %>
|
||||
<br/>
|
||||
<form action="<%= it.removeRegistrationRoute %>" method="POST">
|
||||
<input type="submit" value="<%= it.lang("removeRegistration") %>">
|
||||
</form>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
<h2><%= it.lang("devices") %> (<%= it.devices.length %>)</h2>
|
||||
<ul>
|
||||
<% it.devices.forEach(function(d){ %>
|
||||
<li><%= d.name %> (<%= d.ip %>:<%= d.port %>)</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<h2><%= it.lang("services") %> (<%= it.services.length %>)</h2>
|
||||
<ul>
|
||||
<% it.services.forEach(function(s){ %>
|
||||
<li><%= s.name %> (<%= s.sid %>)</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<div style="margin:20px 0;padding:15px;background:#f8f9fa;border-radius:8px">
|
||||
<h3 style="font-size:1.1rem;margin-bottom:10px;color:#667eea"><%= it.lang("existingServiceConfig") %></h3>
|
||||
<pre style="font-size:0.85rem;text-align:left;overflow-x:auto"><%= JSON.stringify(it.registeredBonobService, null, 2) %></pre>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<p style="color:#999;margin:20px 0"><%= it.lang("noExistingServiceRegistration") %></p>
|
||||
<% } %>
|
||||
|
||||
<% if(it.registeredBonobService) { %>
|
||||
<form action="<%= it.removeRegistrationRoute %>" method="POST" style="margin:20px 0">
|
||||
<input type="submit" value="<%= it.lang("removeRegistration") %>" id="submit" style="background:#dc3545">
|
||||
</form>
|
||||
<% } %>
|
||||
|
||||
<div style="margin-top:40px;padding-top:30px;border-top:2px solid #e0e0e0">
|
||||
<h2 style="font-size:1.5rem;margin-bottom:15px"><%= it.lang("devices") %> (<%= it.devices.length %>)</h2>
|
||||
<% if(it.devices.length > 0) { %>
|
||||
<ul style="list-style:none;text-align:left;padding:0">
|
||||
<% it.devices.forEach(function(d){ %>
|
||||
<li style="padding:10px;margin:5px 0;background:#f8f9fa;border-radius:6px">
|
||||
<strong><%= d.name %></strong> <span style="color:#999">(<%= d.ip %>:<%= d.port %>)</span>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<% } else { %>
|
||||
<p style="color:#999">No devices found</p>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:30px">
|
||||
<h2 style="font-size:1.5rem;margin-bottom:15px"><%= it.lang("services") %> (<%= it.services.length %>)</h2>
|
||||
<% if(it.services.length > 0) { %>
|
||||
<ul style="list-style:none;text-align:left;padding:0">
|
||||
<% it.services.forEach(function(s){ %>
|
||||
<li style="padding:10px;margin:5px 0;background:#f8f9fa;border-radius:6px">
|
||||
<strong><%= s.name %></strong> <span style="color:#999">(SID: <%= s.sid %>)</span>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<% } else { %>
|
||||
<p style="color:#999">No services registered</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,46 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= it.title || "bonob" %></title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
div#content {
|
||||
margin: auto;
|
||||
width: 60%;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 50px 40px;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 700%;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 300%;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 300%;
|
||||
width: 80%;
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 15px 20px;
|
||||
font-size: 1rem;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
input#submit {
|
||||
margin-top: 100px
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 10px;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.one-word-per-line {
|
||||
word-spacing: 100000px;
|
||||
input#submit:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
|
||||
.login{
|
||||
width: min-intrinsic;
|
||||
width: -webkit-min-content;
|
||||
width: -moz-min-content;
|
||||
width: min-content;
|
||||
display: table-caption;
|
||||
display: -ms-grid;
|
||||
-ms-grid-columns: min-content;
|
||||
input#submit:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: 3rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Success and failure page styles */
|
||||
.message-container {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.message-container h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-container p {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Index page styles */
|
||||
.index-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.index-content h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.index-content p {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.index-content a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.index-content a:hover {
|
||||
color: #764ba2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 500px) {
|
||||
div#content {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<% layout('./layout', { title: it.lang("login") }) %>
|
||||
|
||||
<div id="content">
|
||||
<h1 class="login one-word-per-line"><%= it.lang("logInToBonob") %></h1>
|
||||
<div class="logo">🎵</div>
|
||||
<h1><%= it.lang("logInToBonob") %></h1>
|
||||
<form action="<%= it.loginRoute %>" method="POST">
|
||||
<label for="username"><%= it.lang("username") %>:</label><br>
|
||||
<input type="text" id="username" name="username"><br><br>
|
||||
<label for="password"><%= it.lang("password") %>:</label><br>
|
||||
<input type="password" id="password" name="password"><br>
|
||||
<div class="form-group">
|
||||
<label for="username"><%= it.lang("username") %></label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password"><%= it.lang("password") %></label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<input type="hidden" name="linkCode" value="<%= it.linkCode %>">
|
||||
<input type="submit" value="<%= it.lang("login") %>" id="submit">
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<% layout('./layout', { title: it.lang("success") }) %>
|
||||
|
||||
<div id="content">
|
||||
<h1 class="success"><%= it.message %></h1>
|
||||
<div class="message-container">
|
||||
<div class="logo">✓</div>
|
||||
<h1><%= it.message %></h1>
|
||||
<p style="color: #28a745; font-weight: 600; margin-top: 10px;"><%= it.lang("success") %></p>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user