mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-22 09:53:32 +01:00
Replace json config with SQLite database
This commit is contained in:
15
src/app.ts
15
src/app.ts
@@ -18,7 +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";
|
||||
import { SQLiteSmapiTokenStore } from "./smapi_token_store";
|
||||
|
||||
const config = readConfig();
|
||||
const clock = SystemClock;
|
||||
@@ -82,6 +82,16 @@ const version = fs.existsSync(GIT_INFO)
|
||||
? fs.readFileSync(GIT_INFO).toString().trim()
|
||||
: "v??";
|
||||
|
||||
// Initialize SQLite token store
|
||||
const smapiTokenStore = new SQLiteSmapiTokenStore(config.tokenStore.dbPath);
|
||||
|
||||
// Migrate existing JSON tokens if they exist
|
||||
const legacyJsonPath = "/config/tokens.json";
|
||||
if (fs.existsSync(legacyJsonPath)) {
|
||||
logger.info(`Found legacy JSON token file at ${legacyJsonPath}, attempting migration...`);
|
||||
smapiTokenStore.migrateFromJSON(legacyJsonPath);
|
||||
}
|
||||
|
||||
const app = server(
|
||||
sonosSystem,
|
||||
bonob,
|
||||
@@ -97,7 +107,7 @@ const app = server(
|
||||
version,
|
||||
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
|
||||
externalImageResolver: artistImageFetcher,
|
||||
smapiTokenStore: new FileSmapiTokenStore("/config/tokens.json")
|
||||
smapiTokenStore
|
||||
}
|
||||
);
|
||||
|
||||
@@ -126,6 +136,7 @@ process.on('SIGTERM', () => {
|
||||
expressServer.close(() => {
|
||||
logger.info('HTTP server closed');
|
||||
});
|
||||
smapiTokenStore.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -105,5 +105,8 @@ export default function () {
|
||||
scrobbleTracks: bnbEnvVar<boolean>("SCROBBLE_TRACKS", { default: true, parser: asBoolean }),
|
||||
reportNowPlaying:
|
||||
bnbEnvVar<boolean>("REPORT_NOW_PLAYING", { default: true, parser: asBoolean }),
|
||||
tokenStore: {
|
||||
dbPath: bnbEnvVar<string>("TOKEN_DB_PATH", { default: "/config/tokens.db" })!,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import logger from "./logger";
|
||||
import { SmapiToken, SmapiAuthTokens } from "./smapi_auth";
|
||||
import { either as E } from "fp-ts";
|
||||
|
||||
export { SQLiteSmapiTokenStore } from "./sqlite_smapi_token_store";
|
||||
|
||||
export interface SmapiTokenStore {
|
||||
get(token: string): SmapiToken | undefined;
|
||||
set(token: string, fullSmapiToken: SmapiToken): void;
|
||||
|
||||
200
src/sqlite_smapi_token_store.ts
Normal file
200
src/sqlite_smapi_token_store.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import logger from "./logger";
|
||||
import { SmapiToken, SmapiAuthTokens } from "./smapi_auth";
|
||||
import { either as E } from "fp-ts";
|
||||
import { SmapiTokenStore } from "./smapi_token_store";
|
||||
|
||||
export class SQLiteSmapiTokenStore implements SmapiTokenStore {
|
||||
private db!: Database.Database;
|
||||
private readonly dbPath: string;
|
||||
|
||||
constructor(dbPath: string) {
|
||||
this.dbPath = dbPath;
|
||||
this.initializeDatabase();
|
||||
}
|
||||
|
||||
private initializeDatabase(): void {
|
||||
try {
|
||||
// Ensure the directory exists
|
||||
const dir = path.dirname(this.dbPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
logger.info(`Created token storage directory: ${dir}`);
|
||||
}
|
||||
|
||||
// Open database connection
|
||||
this.db = new Database(this.dbPath);
|
||||
|
||||
// Create table if it doesn't exist
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS smapi_tokens (
|
||||
token_key TEXT PRIMARY KEY,
|
||||
token TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Create index for faster lookups
|
||||
this.db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_created_at ON smapi_tokens(created_at)
|
||||
`);
|
||||
|
||||
const count = this.db.prepare("SELECT COUNT(*) as count FROM smapi_tokens").get() as { count: number };
|
||||
logger.info(`SQLite token store initialized at ${this.dbPath} with ${count.count} token(s)`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize SQLite token store at ${this.dbPath}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
get(tokenKey: string): SmapiToken | undefined {
|
||||
try {
|
||||
const stmt = this.db.prepare("SELECT token, key FROM smapi_tokens WHERE token_key = ?");
|
||||
const row = stmt.get(tokenKey) as { token: string; key: string } | undefined;
|
||||
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
token: row.token,
|
||||
key: row.key,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get token from SQLite store`, { error });
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
set(tokenKey: string, fullSmapiToken: SmapiToken): void {
|
||||
try {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT OR REPLACE INTO smapi_tokens (token_key, token, key)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
stmt.run(tokenKey, fullSmapiToken.token, fullSmapiToken.key);
|
||||
logger.debug(`Saved token to SQLite store`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to save token to SQLite store`, { error });
|
||||
}
|
||||
}
|
||||
|
||||
delete(tokenKey: string): void {
|
||||
try {
|
||||
const stmt = this.db.prepare("DELETE FROM smapi_tokens WHERE token_key = ?");
|
||||
stmt.run(tokenKey);
|
||||
logger.debug(`Deleted token from SQLite store`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete token from SQLite store`, { error });
|
||||
}
|
||||
}
|
||||
|
||||
getAll(): { [tokenKey: string]: SmapiToken } {
|
||||
try {
|
||||
const stmt = this.db.prepare("SELECT token_key, token, key FROM smapi_tokens");
|
||||
const rows = stmt.all() as Array<{ token_key: string; token: string; key: string }>;
|
||||
|
||||
const tokens: { [tokenKey: string]: SmapiToken } = {};
|
||||
for (const row of rows) {
|
||||
tokens[row.token_key] = {
|
||||
token: row.token,
|
||||
key: row.key,
|
||||
};
|
||||
}
|
||||
|
||||
return tokens;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get all tokens from SQLite store`, { error });
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
cleanupExpired(smapiAuthTokens: SmapiAuthTokens): number {
|
||||
try {
|
||||
const tokens = this.getAll();
|
||||
const tokenKeys = Object.keys(tokens);
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const tokenKey of tokenKeys) {
|
||||
const smapiToken = tokens[tokenKey];
|
||||
if (smapiToken) {
|
||||
const verifyResult = smapiAuthTokens.verify(smapiToken);
|
||||
// Only delete if token verification fails with InvalidTokenError
|
||||
// Do NOT delete ExpiredTokenError as those can still be refreshed
|
||||
if (E.isLeft(verifyResult)) {
|
||||
const error = verifyResult.left;
|
||||
// Only delete invalid tokens, not expired ones (which can be refreshed)
|
||||
if (error._tag === 'InvalidTokenError') {
|
||||
logger.debug(`Deleting invalid token from SQLite store`);
|
||||
this.delete(tokenKey);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
logger.info(`Cleaned up ${deletedCount} invalid token(s) from SQLite store`);
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to cleanup expired tokens from SQLite store`, { error });
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate tokens from a JSON file to the SQLite database
|
||||
* @param jsonFilePath Path to the JSON file containing tokens
|
||||
* @returns Number of tokens migrated
|
||||
*/
|
||||
migrateFromJSON(jsonFilePath: string): number {
|
||||
try {
|
||||
if (!fs.existsSync(jsonFilePath)) {
|
||||
logger.info(`No JSON token file found at ${jsonFilePath}, skipping migration`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(jsonFilePath, "utf8");
|
||||
const tokens: { [tokenKey: string]: SmapiToken } = JSON.parse(data);
|
||||
const tokenKeys = Object.keys(tokens);
|
||||
|
||||
let migratedCount = 0;
|
||||
for (const tokenKey of tokenKeys) {
|
||||
const token = tokens[tokenKey];
|
||||
if (token) {
|
||||
this.set(tokenKey, token);
|
||||
migratedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Migrated ${migratedCount} token(s) from ${jsonFilePath} to SQLite`);
|
||||
|
||||
// Optionally rename the old JSON file to .bak
|
||||
const backupPath = `${jsonFilePath}.bak`;
|
||||
fs.renameSync(jsonFilePath, backupPath);
|
||||
logger.info(`Backed up original JSON file to ${backupPath}`);
|
||||
|
||||
return migratedCount;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to migrate tokens from JSON file ${jsonFilePath}`, { error });
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
close(): void {
|
||||
try {
|
||||
this.db.close();
|
||||
logger.info("SQLite token store connection closed");
|
||||
} catch (error) {
|
||||
logger.error("Failed to close SQLite token store connection", { error });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user