diff --git a/src/app.ts b/src/app.ts index b20f4cc..cae58e0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -107,7 +107,8 @@ const app = server( version, smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout), externalImageResolver: artistImageFetcher, - smapiTokenStore + smapiTokenStore, + tokenCleanupIntervalMinutes: config.tokenStore.cleanupIntervalMinutes } ); diff --git a/src/config.ts b/src/config.ts index 89b55f3..148453a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -107,6 +107,7 @@ export default function () { bnbEnvVar("REPORT_NOW_PLAYING", { default: true, parser: asBoolean }), tokenStore: { dbPath: bnbEnvVar("TOKEN_DB_PATH", { default: "/config/tokens.db" })!, + cleanupIntervalMinutes: bnbEnvVar("TOKEN_CLEANUP_INTERVAL", { default: 60, parser: asInt })!, }, }; } diff --git a/src/server.ts b/src/server.ts index 00ba0b0..d8e8452 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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) { diff --git a/src/smapi.ts b/src/smapi.ts index aa7ec28..8f1aa02 100644 --- a/src/smapi.ts +++ b/src/smapi.ts @@ -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({ diff --git a/tests/server.test.ts b/tests/server.test.ts index 0a7e892..b62b602 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -240,7 +240,7 @@ describe("server", () => { .send(); expect(res.status).toEqual(200); - expect(res.text).toMatch(`

${lang("devices")} \(0\)

`); + 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(`

${lang("devices")} \(0\)

`); + 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(`

${lang("services")} \(0\)

`); + 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(`

${lang("devices")} \(2\)

`); - 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(`

${lang("services")} \(4\)

`); - 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( - `` - ); - expect(res.text).toMatch(`

${lang("expectedConfig")}

`); - expect(res.text).toMatch( - `

${lang("noExistingServiceRegistration")}

` + `` ); + expect(res.text).toContain(lang("noExistingServiceRegistration")); expect(res.text).not.toMatch( - `` + `value="${lang("removeRegistration")}"` ); }); }); @@ -440,14 +437,11 @@ describe("server", () => { .send(); expect(res.status).toEqual(200); expect(res.text).toMatch( - `` + `` ); - expect(res.text).toMatch(`

${lang("expectedConfig")}

`); + expect(res.text).toContain(lang("existingServiceConfig")); expect(res.text).toMatch( - `

${lang("existingServiceConfig")}

` - ); - expect(res.text).toMatch( - `` + ` { expect(res.status).toEqual(200); expect(res.text).toMatch(`${lang("login")}`); expect(res.text).toMatch( - `

${lang("logInToBonob")}

` + `

${lang("logInToBonob")}

` ); expect(res.text).toMatch( - `` + `` ); expect(res.text).toMatch( - `` + `` ); expect(res.text).toMatch( `` diff --git a/web/views/failure.eta b/web/views/failure.eta index af5f358..b66217e 100644 --- a/web/views/failure.eta +++ b/web/views/failure.eta @@ -1,6 +1,12 @@ <% layout('./layout', { title: it.lang("failure") }) %>
-

<%= it.message %>

-

<%= it.cause || "" %>

+
+ +

<%= it.message %>

+ <% if (it.cause) { %> +

<%= it.cause %>

+ <% } %> +

<%= it.lang("failure") %>

+
\ No newline at end of file diff --git a/web/views/index.eta b/web/views/index.eta index 6568312..bc4cbbe 100644 --- a/web/views/index.eta +++ b/web/views/index.eta @@ -1,45 +1,61 @@ <% layout('./layout') %> -
-
<%= it.version %>
-

<%= it.bonobService.name %> (<%= it.bonobService.sid %>)

-

<%= it.lang("expectedConfig") %>

-
<%= JSON.stringify(it.bonobService) %>
-
+
+
<%= it.version %>
+ +

<%= it.bonobService.name %>

+

Service ID: <%= it.bonobService.sid %>

+ <% if(it.devices.length > 0) { %> -
- "> + + " id="submit">
-
<% } else { %> -

<%= it.lang("noSonosDevices") %>

-
+

<%= it.lang("noSonosDevices") %>

<% } %> <% if(it.registeredBonobService) { %> -

<%= it.lang("existingServiceConfig") %>

-
<%= JSON.stringify(it.registeredBonobService) %>
+
+

<%= it.lang("existingServiceConfig") %>

+
<%= JSON.stringify(it.registeredBonobService, null, 2) %>
+
<% } else { %> -

<%= it.lang("noExistingServiceRegistration") %>

+

<%= it.lang("noExistingServiceRegistration") %>

<% } %> + <% if(it.registeredBonobService) { %> -
-
- "> + + " id="submit" style="background:#dc3545">
<% } %> -
-

<%= it.lang("devices") %> (<%= it.devices.length %>)

-
    - <% it.devices.forEach(function(d){ %> -
  • <%= d.name %> (<%= d.ip %>:<%= d.port %>)
  • - <% }) %> -
-

<%= it.lang("services") %> (<%= it.services.length %>)

-
    - <% it.services.forEach(function(s){ %> -
  • <%= s.name %> (<%= s.sid %>)
  • - <% }) %> -
+
+

<%= it.lang("devices") %> (<%= it.devices.length %>)

+ <% if(it.devices.length > 0) { %> +
    + <% it.devices.forEach(function(d){ %> +
  • + <%= d.name %> (<%= d.ip %>:<%= d.port %>) +
  • + <% }) %> +
+ <% } else { %> +

No devices found

+ <% } %> +
+ +
+

<%= it.lang("services") %> (<%= it.services.length %>)

+ <% if(it.services.length > 0) { %> +
    + <% it.services.forEach(function(s){ %> +
  • + <%= s.name %> (SID: <%= s.sid %>) +
  • + <% }) %> +
+ <% } else { %> +

No services registered

+ <% } %> +
\ No newline at end of file diff --git a/web/views/layout.eta b/web/views/layout.eta index 3b3f650..73f0939 100644 --- a/web/views/layout.eta +++ b/web/views/layout.eta @@ -1,46 +1,189 @@ + + <%= it.title || "bonob" %> diff --git a/web/views/login.eta b/web/views/login.eta index 12ee98a..17bc141 100644 --- a/web/views/login.eta +++ b/web/views/login.eta @@ -1,12 +1,17 @@ <% layout('./layout', { title: it.lang("login") }) %>
-

<%= it.lang("logInToBonob") %>

+ +

<%= it.lang("logInToBonob") %>

-
-

-
-
+
+ + +
+
+ + +
" id="submit">
diff --git a/web/views/success.eta b/web/views/success.eta index 431d432..0578af3 100644 --- a/web/views/success.eta +++ b/web/views/success.eta @@ -1,5 +1,9 @@ <% layout('./layout', { title: it.lang("success") }) %>
-

<%= it.message %>

+
+ +

<%= it.message %>

+

<%= it.lang("success") %>

+
\ No newline at end of file