mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
201 lines
6.1 KiB
TypeScript
201 lines
6.1 KiB
TypeScript
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;
|
|
// Delete both invalid and expired tokens to prevent accumulation
|
|
if (error._tag === 'InvalidTokenError' || error._tag === 'ExpiredTokenError') {
|
|
logger.debug(`Deleting ${error._tag} 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 });
|
|
}
|
|
}
|
|
}
|