Save tokens

This commit is contained in:
Wolfgang Kulhanek
2025-10-16 10:51:40 +02:00
parent 01f0dc942b
commit fee5f74a2c
7 changed files with 366 additions and 38 deletions

View File

@@ -18,6 +18,7 @@ import sonos, { bonobService } from "./sonos";
import { MusicService } from "./music_service";
import { SystemClock } from "./clock";
import { JWTSmapiLoginTokens } from "./smapi_auth";
import { FileSmapiTokenStore } from "./smapi_token_store";
const config = readConfig();
const clock = SystemClock;
@@ -95,7 +96,8 @@ const app = server(
logRequests: config.logRequests,
version,
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
externalImageResolver: artistImageFetcher
externalImageResolver: artistImageFetcher,
smapiTokenStore: new FileSmapiTokenStore("/config/tokens.json")
}
);

View File

@@ -39,6 +39,7 @@ import {
JWTSmapiLoginTokens,
SmapiAuthTokens,
} from "./smapi_auth";
import { SmapiTokenStore, InMemorySmapiTokenStore } from "./smapi_token_store";
export const BONOB_ACCESS_TOKEN_HEADER = "bat";
@@ -92,6 +93,7 @@ export type ServerOpts = {
version: string;
smapiAuthTokens: SmapiAuthTokens;
externalImageResolver: ImageFetcher;
smapiTokenStore: SmapiTokenStore;
};
const DEFAULT_SERVER_OPTS: ServerOpts = {
@@ -108,6 +110,7 @@ const DEFAULT_SERVER_OPTS: ServerOpts = {
"1m"
),
externalImageResolver: axiosImageFetcher,
smapiTokenStore: new InMemorySmapiTokenStore(),
};
function server(
@@ -607,7 +610,8 @@ function server(
apiTokens,
clock,
i8n,
serverOpts.smapiAuthTokens
serverOpts.smapiAuthTokens,
serverOpts.smapiTokenStore
);
if (serverOpts.applyContextPath) {

View File

@@ -40,6 +40,7 @@ import {
} from "./smapi_auth";
import { InvalidTokenError } from "./smapi_auth";
import { IncomingHttpHeaders } from "http2";
import { SmapiTokenStore } from "./smapi_token_store";
export const LOGIN_ROUTE = "/login";
export const CREATE_REGISTRATION_ROUTE = "/registration/add";
@@ -164,20 +165,20 @@ class SonosSoap {
bonobUrl: URLBuilder;
smapiAuthTokens: SmapiAuthTokens;
clock: Clock;
tokens: {[tokenKey:string]:SmapiToken};
tokenStore: SmapiTokenStore;
constructor(
bonobUrl: URLBuilder,
linkCodes: LinkCodes,
smapiAuthTokens: SmapiAuthTokens,
clock: Clock
clock: Clock,
tokenStore: SmapiTokenStore
) {
this.bonobUrl = bonobUrl;
this.linkCodes = linkCodes;
this.smapiAuthTokens = smapiAuthTokens;
this.clock = clock;
this.tokens = {};
this.tokenStore = tokenStore;
}
getAppLink(): GetAppLinkResult {
@@ -244,17 +245,17 @@ class SonosSoap {
};
}
}
getCredentialsForToken(token: string): SmapiToken {
getCredentialsForToken(token: string): SmapiToken | undefined {
logger.debug("getCredentialsForToken called with: " + token);
logger.debug("Current tokens: " + JSON.stringify(this.tokens));
return this.tokens[token]!;
logger.debug("Current tokens: " + JSON.stringify(this.tokenStore.getAll()));
return this.tokenStore.get(token);
}
associateCredentialsForToken(token: string, fullSmapiToken: SmapiToken, oldToken?:string) {
logger.debug("Adding token: " + token + " " + JSON.stringify(fullSmapiToken));
if(oldToken) {
delete this.tokens[oldToken];
this.tokenStore.delete(oldToken);
}
this.tokens[token] = fullSmapiToken;
this.tokenStore.set(token, fullSmapiToken);
}
}
@@ -403,9 +404,10 @@ function bindSmapiSoapServiceToExpress(
apiKeys: APITokens,
clock: Clock,
i8n: I8N,
smapiAuthTokens: SmapiAuthTokens
smapiAuthTokens: SmapiAuthTokens,
tokenStore: SmapiTokenStore
) {
const sonosSoap = new SonosSoap(bonobUrl, linkCodes, smapiAuthTokens, clock);
const sonosSoap = new SonosSoap(bonobUrl, linkCodes, smapiAuthTokens, clock, tokenStore);
const urlWithToken = (accessToken: string) =>
bonobUrl.append({

96
src/smapi_token_store.ts Normal file
View File

@@ -0,0 +1,96 @@
import fs from "fs";
import path from "path";
import logger from "./logger";
import { SmapiToken } from "./smapi_auth";
export interface SmapiTokenStore {
get(token: string): SmapiToken | undefined;
set(token: string, fullSmapiToken: SmapiToken): void;
delete(token: string): void;
getAll(): { [tokenKey: string]: SmapiToken };
}
export class InMemorySmapiTokenStore implements SmapiTokenStore {
private tokens: { [tokenKey: string]: SmapiToken } = {};
get(token: string): SmapiToken | undefined {
return this.tokens[token];
}
set(token: string, fullSmapiToken: SmapiToken): void {
this.tokens[token] = fullSmapiToken;
}
delete(token: string): void {
delete this.tokens[token];
}
getAll(): { [tokenKey: string]: SmapiToken } {
return this.tokens;
}
}
export class FileSmapiTokenStore implements SmapiTokenStore {
private tokens: { [tokenKey: string]: SmapiToken } = {};
private readonly filePath: string;
constructor(filePath: string) {
this.filePath = filePath;
this.loadFromFile();
}
private loadFromFile(): void {
try {
// Ensure the directory exists
const dir = path.dirname(this.filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
logger.info(`Created token storage directory: ${dir}`);
}
// Load existing tokens if file exists
if (fs.existsSync(this.filePath)) {
const data = fs.readFileSync(this.filePath, "utf8");
this.tokens = JSON.parse(data);
logger.info(
`Loaded ${Object.keys(this.tokens).length} token(s) from ${this.filePath}`
);
} else {
logger.info(`No existing token file found at ${this.filePath}, starting fresh`);
this.tokens = {};
this.saveToFile();
}
} catch (error) {
logger.error(`Failed to load tokens from ${this.filePath}`, { error });
this.tokens = {};
}
}
private saveToFile(): void {
try {
const data = JSON.stringify(this.tokens, null, 2);
fs.writeFileSync(this.filePath, data, "utf8");
logger.debug(`Saved ${Object.keys(this.tokens).length} token(s) to ${this.filePath}`);
} catch (error) {
logger.error(`Failed to save tokens to ${this.filePath}`, { error });
}
}
get(token: string): SmapiToken | undefined {
return this.tokens[token];
}
set(token: string, fullSmapiToken: SmapiToken): void {
this.tokens[token] = fullSmapiToken;
this.saveToFile();
}
delete(token: string): void {
delete this.tokens[token];
this.saveToFile();
}
getAll(): { [tokenKey: string]: SmapiToken } {
return this.tokens;
}
}