inject SpotifyTokenService in relevant classes

This commit is contained in:
StarAppeal
2025-09-19 23:19:51 +02:00
parent 92bd66c6f3
commit 65d87b77c6
13 changed files with 80 additions and 43 deletions
+5 -4
View File
@@ -2,10 +2,11 @@ import axios from "axios";
import {OAuthTokenResponse} from "../../interfaces/OAuthTokenResponse";
const url = "https://accounts.spotify.com/api/token";
const clientId = process.env.SPOTIFY_CLIENT_ID;
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
export class SpotifyTokenService {
constructor(private readonly clientId: string, private readonly clientSecret: string) {
}
public async refreshToken(refreshToken: string) {
console.log("refreshToken")
const response = await axios.post(
@@ -15,7 +16,7 @@ export class SpotifyTokenService {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${clientId}:${clientSecret}`,
`${this.clientId}:${this.clientSecret}`,
).toString("base64")}`,
},
},
@@ -33,7 +34,7 @@ export class SpotifyTokenService {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${clientId}:${clientSecret}`,
`${this.clientId}:${this.clientSecret}`,
).toString("base64")}`,
},
},
+19 -5
View File
@@ -15,15 +15,15 @@ import {randomUUID} from "crypto";
import {JwtAuthenticator} from "./utils/jwtAuthenticator";
import {authenticateJwt} from "./rest/middleware/authenticateJwt";
import {disconnectFromDatabase} from "./db/services/db/database.service";
import {SpotifyTokenService} from "./db/services/spotifyTokenService";
export async function startServer(jwtSecret: string) {
export async function startServer(jwtSecret: string, spotifyClientId: string, spotifyClientSecret: string) {
const app = express();
const port = config.port;
app.set("trust proxy", 1);
app.use(cookieParser());
// test
app.use(cors({
origin: config.cors.origin,
credentials: config.cors.credentials,
@@ -47,18 +47,20 @@ export async function startServer(jwtSecret: string) {
const userService = await UserService.create();
console.log("UserService created successfully.");
const spotifyTokenService = new SpotifyTokenService(spotifyClientId, spotifyClientSecret);
const _authenticateJwt = authenticateJwt(new JwtAuthenticator(jwtSecret));
const server = app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
const webSocketServer = new ExtendedWebSocketServer(server, userService);
const webSocketServer = new ExtendedWebSocketServer(server, userService, spotifyTokenService);
const restWebSocket = new RestWebSocket(webSocketServer);
const restUser = new RestUser(userService);
const auth = new RestAuth(userService);
const jwtTokenPropertiesExtractor = new JwtTokenPropertiesExtractor();
const spotify = new SpotifyTokenGenerator();
const spotify = new SpotifyTokenGenerator(spotifyTokenService);
app.use("/api/auth", authLimiter, auth.createRouter());
@@ -119,13 +121,25 @@ export async function startServer(jwtSecret: string) {
if (process.env.NODE_ENV !== 'test') {
const JWT_SECRET = process.env.SECRET_KEY;
const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;
const CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;
if (!JWT_SECRET || JWT_SECRET.length < 32) {
console.error("CRITICAL ERROR: SECRET_KEY environment variable is not set or too short. Aborting.");
process.exit(1);
}
startServer(JWT_SECRET).catch(error => {
if (!CLIENT_ID) {
console.error("CRITICAL ERROR: CLIENT_ID environment variable is not set. Aborting.");
process.exit(1);
}
if (!CLIENT_SECRET) {
console.error("CRITICAL ERROR: CLIENT_SECRET environment variable is not set. Aborting.");
process.exit(1);
}
startServer(JWT_SECRET, CLIENT_ID, CLIENT_SECRET).catch(error => {
console.error("Fatal error during server startup:", error);
process.exit(1);
});
+14 -13
View File
@@ -1,12 +1,13 @@
import express from "express";
import {SpotifyTokenService} from "../db/services/spotifyTokenService";
import { asyncHandler } from "./middleware/asyncHandler";
import { validateBody, v } from "./middleware/validate";
import { ok, internalError } from "./utils/responses";
import {asyncHandler} from "./middleware/asyncHandler";
import {validateBody, v} from "./middleware/validate";
import {ok, internalError} from "./utils/responses";
export class SpotifyTokenGenerator {
private tokenService = new SpotifyTokenService();
constructor(private spotifyTokenService: SpotifyTokenService) {
}
public createRouter() {
const router = express.Router();
@@ -14,29 +15,29 @@ export class SpotifyTokenGenerator {
router.post(
"/token/refresh",
validateBody({
refreshToken: { required: true, validator: v.isString({ nonEmpty: true }) },
refreshToken: {required: true, validator: v.isString({nonEmpty: true})},
}),
asyncHandler(async (req, res) => {
const { refreshToken } = req.body as { refreshToken: string };
const {refreshToken} = req.body as { refreshToken: string };
const token = await this.tokenService.refreshToken(refreshToken);
const token = await this.spotifyTokenService.refreshToken(refreshToken);
return ok(res, { token });
return ok(res, {token});
})
);
router.post(
"/token/generate",
validateBody({
authCode: { required: true, validator: v.isString({ nonEmpty: true }) },
redirectUri: { required: true, validator: v.isUrl() },
authCode: {required: true, validator: v.isString({nonEmpty: true})},
redirectUri: {required: true, validator: v.isUrl()},
}),
asyncHandler(async (req, res) => {
const { authCode, redirectUri } = req.body as { authCode: string; redirectUri: string };
const {authCode, redirectUri} = req.body as { authCode: string; redirectUri: string };
const token = await this.tokenService.generateToken(authCode, redirectUri);
const token = await this.spotifyTokenService.generateToken(authCode, redirectUri);
return ok(res, { token });
return ok(res, {token});
})
);
@@ -4,6 +4,8 @@ import {SpotifyTokenService} from "../../../db/services/spotifyTokenService";
import {getCurrentlyPlaying} from "../../../db/services/spotifyApiService";
import {CustomWebsocketEventUserService} from "./customWebsocketEventUserService";
import {NoData} from "./NoData";
import {UserService} from "../../../db/services/db/UserService";
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket";
export const SpotifyAsyncUpdateEvent = "SPOTIFY_UPDATE";
@@ -29,8 +31,15 @@ export class GetSpotifyUpdatesEvent extends CustomWebsocketEvent<NoData> {
export class GetSingleSpotifyUpdateEvent extends CustomWebsocketEventUserService<NoData> {
private readonly spotifyTokenService: SpotifyTokenService;
event = WebsocketEventType.GET_SINGLE_SPOTIFY_UPDATE;
constructor(ws: ExtendedWebSocket, userService: UserService, spotifyTokenService: SpotifyTokenService) {
super(ws, userService);
this.spotifyTokenService = spotifyTokenService;
}
handler = async () => {
console.log("Getting single Spotify update event");
await this.spotifyUpdates();
@@ -49,7 +58,7 @@ export class GetSingleSpotifyUpdateEvent extends CustomWebsocketEventUserService
if (Date.now() > spotifyConfig.expirationDate.getTime()) {
console.log("Token expired");
const token = await new SpotifyTokenService().refreshToken(spotifyConfig.refreshToken);
const token = await this.spotifyTokenService.refreshToken(spotifyConfig.refreshToken);
const newSpotifyConfig = {
// use old refresh token because you don't get a new one
refreshToken: user.spotifyConfig!.refreshToken,
@@ -58,9 +67,10 @@ export class GetSingleSpotifyUpdateEvent extends CustomWebsocketEventUserService
scope: token.scope,
};
await this.userService.updateUserById(user.id, {spotifyConfig: newSpotifyConfig});
this.ws.user.spotifyConfig = newSpotifyConfig;
console.log("Token refreshed and database updated");
}
const musicData = await getCurrentlyPlaying(user.spotifyConfig!.accessToken);
const musicData = await getCurrentlyPlaying(this.ws.user.spotifyConfig!.accessToken);
if (!musicData) {
console.log("No music data found, maybe error from spotify, skipping this update");
return;
@@ -10,12 +10,13 @@ import {UpdateUserEvent, UpdateUserSingleEvent} from "./updateUserEvent";
import {CustomWebsocketEvent} from "./customWebsocketEvent";
import {StopUpdateUserEvent} from "./stopUpdateUserEvent";
import {UserService} from "../../../db/services/db/UserService";
import {SpotifyTokenService} from "../../../db/services/spotifyTokenService";
export function getEventListeners(ws: ExtendedWebSocket, userService: UserService): CustomWebsocketEvent[] {
export function getEventListeners(ws: ExtendedWebSocket, userService: UserService, spotifyTokenService: SpotifyTokenService): CustomWebsocketEvent[] {
return [
new GetStateEvent(ws),
new GetSettingsEvent(ws),
new GetSingleSpotifyUpdateEvent(ws, userService),
new GetSingleSpotifyUpdateEvent(ws, userService, spotifyTokenService),
new GetSpotifyUpdatesEvent(ws),
new StopSpotifyUpdatesEvent(ws),
new GetSingleWeatherUpdateEvent(ws),
+3 -2
View File
@@ -2,9 +2,10 @@ import {ExtendedWebSocket} from "../../interfaces/extendedWebsocket";
import {CustomWebsocketEvent} from "./websocketCustomEvents/customWebsocketEvent";
import {UserService} from "../../db/services/db/UserService";
import {getEventListeners} from "./websocketCustomEvents/websocketEventUtils";
import {SpotifyTokenService} from "../../db/services/spotifyTokenService";
export class WebsocketEventHandler {
constructor(private webSocket: ExtendedWebSocket, private userService: UserService) {
constructor(private webSocket: ExtendedWebSocket, private userService: UserService, private spotifyTokenService: SpotifyTokenService) {
}
public enableErrorEvent() {
@@ -45,7 +46,7 @@ export class WebsocketEventHandler {
}
public registerCustomEvents() {
const events = getEventListeners(this.webSocket, this.userService);
const events = getEventListeners(this.webSocket, this.userService, this.spotifyTokenService);
events.forEach(this.registerCustomEvent, this);
}
+5 -2
View File
@@ -7,18 +7,21 @@ import {WebsocketServerEventHandler} from "./utils/websocket/websocketServerEven
import {WebsocketEventHandler} from "./utils/websocket/websocketEventHandler";
import {WebsocketEventType} from "./utils/websocket/websocketCustomEvents/websocketEventType";
import {UserService} from "./db/services/db/UserService";
import {SpotifyTokenService} from "./db/services/spotifyTokenService";
export class ExtendedWebSocketServer {
private readonly _wss: WebSocketServer;
private readonly userService: UserService;
private readonly spotifyTokenService: SpotifyTokenService;
constructor(server: Server, userService: UserService) {
constructor(server: Server, userService: UserService, spotifyTokenService: SpotifyTokenService) {
this._wss = new WebSocketServer({
server,
verifyClient: (info, callback) => verifyClient(info.req, callback),
});
this.userService = userService;
this.spotifyTokenService = spotifyTokenService;
this.setupWebSocket();
}
@@ -55,7 +58,7 @@ export class ExtendedWebSocketServer {
private setupWebSocket() {
const serverEventHandler = new WebsocketServerEventHandler(this.wss, this.userService);
serverEventHandler.enableConnectionEvent((ws) => {
const socketEventHandler = new WebsocketEventHandler(ws, this.userService);
const socketEventHandler = new WebsocketEventHandler(ws, this.userService, this.spotifyTokenService);
console.log("WebSocket client connected");
@@ -17,11 +17,8 @@ describe("SpotifyTokenService - Successful Initialization", () => {
let spotifyTokenService: SpotifyTokenServiceType;
beforeEach(async () => {
vi.stubEnv("SPOTIFY_CLIENT_ID", "test-client-id");
vi.stubEnv("SPOTIFY_CLIENT_SECRET", "test-client-secret");
const { SpotifyTokenService } = await import("../../../src/db/services/spotifyTokenService");
spotifyTokenService = new SpotifyTokenService();
spotifyTokenService = new SpotifyTokenService("test-client-id","test-client-secret");
});
const getExpectedAuthHeader = () => {
+1 -1
View File
@@ -58,7 +58,7 @@ let server: http.Server;
beforeAll(async () => {
const { startServer } = await import("../src/index");
const instances = await startServer("not-used");
const instances = await startServer("not-used", "not-used", "not-used");
app = instances.app;
server = instances.server;
});
+1 -3
View File
@@ -17,9 +17,7 @@ describe("SpotifyTokenGenerator", () => {
mockTokenService = createMockSpotifyTokenService();
vi.mocked(SpotifyTokenService).mockImplementation(() => mockTokenService as any);
const spotifyGenerator = new SpotifyTokenGenerator();
const spotifyGenerator = new SpotifyTokenGenerator(mockTokenService as any);
app = createTestApp(spotifyGenerator.createRouter(), "/spotify");
});
@@ -6,6 +6,7 @@ import type { UserService } from "../../../../src/db/services/db/UserService";
import {
CustomWebsocketEventUserService
} from "../../../../src/utils/websocket/websocketCustomEvents/customWebsocketEventUserService";
import {createMockSpotifyTokenService} from "../../../helpers/testSetup";
type MockWs = {
user: {
@@ -41,7 +42,9 @@ describe("websocketEventUtils.getEventListeners", () => {
updateUser: vi.fn(),
} as any;
listeners = getEventListeners(mockWs as any, mockUserService);
listeners = getEventListeners(mockWs as any, mockUserService, createMockSpotifyTokenService() as any);
});
it("should return an array of event listener objects", () => {
@@ -3,11 +3,13 @@ import { WebsocketEventHandler } from "../../../src/utils/websocket/websocketEve
import { ExtendedWebSocket } from "../../../src/interfaces/extendedWebsocket";
import { CustomWebsocketEvent } from "../../../src/utils/websocket/websocketCustomEvents/customWebsocketEvent";
import {UserService} from "../../../src/db/services/db/UserService";
import {SpotifyTokenService} from "../../../src/db/services/spotifyTokenService";
describe("WebsocketEventHandler", () => {
let mockWebSocket: Mocked<ExtendedWebSocket>;
let websocketEventHandler: WebsocketEventHandler;
let mockUserService: Mocked<UserService>;
let mockSpotifyTokenService: Mocked<SpotifyTokenService>
let registeredHandlers: Map<string, (...args: any[]) => void>;
beforeEach(() => {
@@ -31,8 +33,10 @@ describe("WebsocketEventHandler", () => {
// not used in this test
mockUserService = {} as Mocked<UserService>;
mockSpotifyTokenService = {} as Mocked<SpotifyTokenService>;
websocketEventHandler = new WebsocketEventHandler(mockWebSocket, mockUserService);
websocketEventHandler = new WebsocketEventHandler(mockWebSocket, mockUserService, mockSpotifyTokenService);
});
afterEach(() => {
+7 -3
View File
@@ -6,7 +6,8 @@ import { WebsocketServerEventHandler } from "../src/utils/websocket/websocketSer
import { WebsocketEventHandler } from "../src/utils/websocket/websocketEventHandler";
import { getEventListeners } from "../src/utils/websocket/websocketCustomEvents/websocketEventUtils";
import {UserService} from "../src/db/services/db/UserService";
import {createMockUserService} from "./helpers/testSetup";
import {createMockSpotifyTokenService, createMockUserService} from "./helpers/testSetup";
import {SpotifyTokenService} from "../src/db/services/spotifyTokenService";
let mockWssInstance: Mocked<WebSocketServer>;
let mockServerEventHandler: Mocked<WebsocketServerEventHandler>;
@@ -27,6 +28,7 @@ describe("ExtendedWebSocketServer", () => {
let mockHttpServer: Mocked<Server>;
let extendedWss: ExtendedWebSocketServer;
let mockUserService: Mocked<UserService>;
let mockSpotifyService : Mocked<SpotifyTokenService>;
beforeEach(() => {
vi.clearAllMocks();
@@ -46,8 +48,10 @@ describe("ExtendedWebSocketServer", () => {
} as unknown as Mocked<WebSocketServer>;
mockUserService = createMockUserService();
mockSpotifyService = createMockSpotifyTokenService() as any;
extendedWss = new ExtendedWebSocketServer(mockHttpServer, mockUserService);
extendedWss = new ExtendedWebSocketServer(mockHttpServer, mockUserService, mockSpotifyService);
});
describe("Constructor and Setup", () => {
@@ -117,7 +121,7 @@ describe("ExtendedWebSocketServer", () => {
it("should create and configure a WebsocketEventHandler for new clients", () => {
connectionHandler(mockWsClient, {});
expect(vi.mocked(WebsocketEventHandler)).toHaveBeenCalledWith(mockWsClient, mockUserService);
expect(vi.mocked(WebsocketEventHandler)).toHaveBeenCalledWith(mockWsClient, mockUserService, mockSpotifyService);
expect(mockClientEventHandler.enableErrorEvent).toHaveBeenCalled();
expect(mockClientEventHandler.enablePongEvent).toHaveBeenCalled();
expect(mockClientEventHandler.enableMessageEvent).toHaveBeenCalled();