diff --git a/src/config/config.ts b/src/config/config.ts index 3bee9e0..f9946e3 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -38,7 +38,7 @@ function isValidUrl(u: string): boolean { } } -const NODE_ENV = (optionalString("NODE_ENV", process.env.NODE_ENV, "development") as NodeEnv); +const NODE_ENV = optionalString("NODE_ENV", process.env.NODE_ENV, "development") as NodeEnv; const PORT = optionalNumber("PORT", process.env.PORT, 3000); const FRONTEND_URL = required("FRONTEND_URL", process.env.FRONTEND_URL); @@ -46,7 +46,7 @@ if (!isValidUrl(FRONTEND_URL)) { throw new Error("FRONTEND_URL must be a valid URL"); } -export const config :BaseConfig = { +export const config: BaseConfig = { env: NODE_ENV, port: PORT, cors: { diff --git a/src/db/models/user.ts b/src/db/models/user.ts index c92e8f8..8dcc460 100644 --- a/src/db/models/user.ts +++ b/src/db/models/user.ts @@ -1,6 +1,6 @@ import "dotenv/config"; -import mongoose, {Schema, Document} from "mongoose"; -import {PasswordUtils} from "../../utils/passwordUtils"; +import mongoose, { Schema, Document } from "mongoose"; +import { PasswordUtils } from "../../utils/passwordUtils"; export interface IUser extends Document { name: string; @@ -14,10 +14,10 @@ export interface IUser extends Document { } export interface CreateUserPayload { - name: string, - password: string, - uuid: string, - config: UserConfig, + name: string; + password: string; + uuid: string; + config: UserConfig; timezone: string; location: string; } @@ -30,12 +30,12 @@ export interface UserConfig { export interface MatrixState { global: { - mode: 'image' | 'text' | "idle" | "music" | "clock"; + mode: "image" | "text" | "idle" | "music" | "clock"; brightness: number; }; text: { text: string; - align: 'left' | 'center' | 'right'; + align: "left" | "center" | "right"; speed: number; size: number; color: [number, number, number]; @@ -58,71 +58,83 @@ export interface SpotifyConfig { scope: string; } -const matrixStateSchema = new Schema({ - global: { - mode: {type: String, enum: ['image', 'text', 'idle', 'music', 'clock'], default: 'idle'}, - brightness: {type: Number, min: 0, max: 100, default: 50}, - }, - text: { - text: {type: String, default: ""}, - align: {type: String, enum: ['left', 'center', 'right'], default: 'center'}, - speed: {type: Number, min: 0, max: 10, default: 3}, - size: {type: Number, min: 1, max: 64, default: 12}, - color: { - type: [Number], - validate: { - validator: (v: number[]) => - Array.isArray(v) && v.length === 3 && v.every(n => Number.isInteger(n) && n >= 0 && n <= 255), - message: "color must be an array of three integers between 0 and 255", +const matrixStateSchema = new Schema( + { + global: { + mode: { type: String, enum: ["image", "text", "idle", "music", "clock"], default: "idle" }, + brightness: { type: Number, min: 0, max: 100, default: 50 }, + }, + text: { + text: { type: String, default: "" }, + align: { type: String, enum: ["left", "center", "right"], default: "center" }, + speed: { type: Number, min: 0, max: 10, default: 3 }, + size: { type: Number, min: 1, max: 64, default: 12 }, + color: { + type: [Number], + validate: { + validator: (v: number[]) => + Array.isArray(v) && v.length === 3 && v.every((n) => Number.isInteger(n) && n >= 0 && n <= 255), + message: "color must be an array of three integers between 0 and 255", + }, + default: [255, 255, 255], }, - default: [255, 255, 255], + }, + image: { + image: { type: String, default: "" }, + }, + clock: { + color: { + type: [Number], + validate: { + validator: (v: number[]) => + Array.isArray(v) && v.length === 3 && v.every((n) => Number.isInteger(n) && n >= 0 && n <= 255), + message: "color must be an array of three integers between 0 and 255", + }, + default: [255, 255, 255], + }, + }, + music: { + fullscreen: { type: Boolean, default: false }, }, }, - image: { - image: {type: String, default: ""}, - }, - clock: { - color: { - type: [Number], - validate: { - validator: (v: number[]) => - Array.isArray(v) && v.length === 3 && v.every(n => Number.isInteger(n) && n >= 0 && n <= 255), - message: "color must be an array of three integers between 0 and 255", - }, - default: [255, 255, 255], - }, - }, - music: { - fullscreen: {type: Boolean, default: false}, - }, -}, {_id: false}); + { _id: false } +); -const spotifyConfigSchema = new Schema({ - accessToken: {type: String}, - refreshToken: {type: String}, - expirationDate: {type: Date}, - scope: {type: String}, -}, {_id: false}); +const spotifyConfigSchema = new Schema( + { + accessToken: { type: String }, + refreshToken: { type: String }, + expirationDate: { type: Date }, + scope: { type: String }, + }, + { _id: false } +); -const userConfigSchema = new Schema({ - isVisible: {type: Boolean, required: true}, - canBeModified: {type: Boolean, required: true}, - isAdmin: {type: Boolean, required: true}, -}, {_id: false}); +const userConfigSchema = new Schema( + { + isVisible: { type: Boolean, required: true }, + canBeModified: { type: Boolean, required: true }, + isAdmin: { type: Boolean, required: true }, + }, + { _id: false } +); -const userSchema = new Schema({ - name: {type: String, required: true, index: true}, - password: {type: String, required: true, select: false}, - uuid: {type: String, required: true, unique: true, index: true}, - config: {type: userConfigSchema, required: true}, - lastState: {type: matrixStateSchema}, - spotifyConfig: {type: spotifyConfigSchema}, - timezone: {type: String, required: true}, - location: {type: String, required: true}, -}, { - optimisticConcurrency: true, - timestamps: true, -}); +const userSchema = new Schema( + { + name: { type: String, required: true, index: true }, + password: { type: String, required: true, select: false }, + uuid: { type: String, required: true, unique: true, index: true }, + config: { type: userConfigSchema, required: true }, + lastState: { type: matrixStateSchema }, + spotifyConfig: { type: spotifyConfigSchema }, + timezone: { type: String, required: true }, + location: { type: String, required: true }, + }, + { + optimisticConcurrency: true, + timestamps: true, + } +); userSchema.virtual("id").get(function (this: any) { return this._id?.toHexString?.() ?? this._id; @@ -136,7 +148,7 @@ async function hashIfNeeded(next: Function, user: any) { if (!user.isModified?.("password")) return next(); if (isBcryptHash(user.password)) return next(); try { - user.password = await PasswordUtils.hashPassword(user.password) + user.password = await PasswordUtils.hashPassword(user.password); return next(); } catch (e) { return next(e); @@ -165,4 +177,4 @@ userSchema.pre("findOneAndUpdate", async function (next) { } }); -export const UserModel = mongoose.model('User', userSchema); +export const UserModel = mongoose.model("User", userSchema); diff --git a/src/db/models/userWatch.ts b/src/db/models/userWatch.ts index 49136de..ede8180 100644 --- a/src/db/models/userWatch.ts +++ b/src/db/models/userWatch.ts @@ -1,11 +1,11 @@ -import { UserModel } from './user'; -import {appEventBus, USER_UPDATED_EVENT} from "../../utils/eventBus"; +import { UserModel } from "./user"; +import { appEventBus, USER_UPDATED_EVENT } from "../../utils/eventBus"; export function watchUserChanges() { - const changeStream = UserModel.watch([], { fullDocument: 'updateLookup' }); + const changeStream = UserModel.watch([], { fullDocument: "updateLookup" }); - changeStream.on('change', (change: any) => { - if (change.operationType === 'update' && change.fullDocument) { + changeStream.on("change", (change: any) => { + if (change.operationType === "update" && change.fullDocument) { const updatedUser = change.fullDocument; appEventBus.emit(USER_UPDATED_EVENT, updatedUser); diff --git a/src/index.ts b/src/index.ts index 5584253..67e0c83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,13 @@ -import {Server} from "./server"; -import {config as baseConfig} from "./config/config"; -import {S3Service} from "./services/s3Service"; -import {UserService} from "./services/db/UserService"; -import {SpotifyTokenService} from "./services/spotifyTokenService"; -import {connectToDatabase} from "./services/db/database.service"; -import {SpotifyApiService} from "./services/spotifyApiService"; -import {SpotifyPollingService} from "./services/spotifyPollingService"; -import {WeatherPollingService} from "./services/weatherPollingService"; -import {JwtAuthenticator} from "./utils/jwtAuthenticator"; +import { Server } from "./server"; +import { config as baseConfig } from "./config/config"; +import { S3Service } from "./services/s3Service"; +import { UserService } from "./services/db/UserService"; +import { SpotifyTokenService } from "./services/spotifyTokenService"; +import { connectToDatabase } from "./services/db/database.service"; +import { SpotifyApiService } from "./services/spotifyApiService"; +import { SpotifyPollingService } from "./services/spotifyPollingService"; +import { WeatherPollingService } from "./services/weatherPollingService"; +import { JwtAuthenticator } from "./utils/jwtAuthenticator"; async function bootstrap() { const { @@ -20,7 +20,7 @@ async function bootstrap() { MINIO_ROOT_USER, MINIO_ROOT_PASSWORD, DB_NAME, - DB_CONN_STRING + DB_CONN_STRING, } = process.env; if (!SECRET_KEY || SECRET_KEY.length < 32) { @@ -54,22 +54,19 @@ async function bootstrap() { port: parseInt(MINIO_PORT), accessKey: MINIO_ROOT_USER, secretAccessKey: MINIO_ROOT_PASSWORD, - bucket: MINIO_BUCKET_NAME + bucket: MINIO_BUCKET_NAME, }; const dbConfig = { dbName: DB_NAME, - dbConnString: DB_CONN_STRING - } + dbConnString: DB_CONN_STRING, + }; await connectToDatabase(dbConfig.dbName, dbConfig.dbConnString); const s3Service = S3Service.getInstance(s3ClientConfig); const userService = await UserService.create(); - const spotifyTokenService = new SpotifyTokenService( - SPOTIFY_CLIENT_ID!, - SPOTIFY_CLIENT_SECRET! - ); + const spotifyTokenService = new SpotifyTokenService(SPOTIFY_CLIENT_ID!, SPOTIFY_CLIENT_SECRET!); const spotifyApiService = new SpotifyApiService(); const spotifyPollingService = new SpotifyPollingService(userService, spotifyApiService, spotifyTokenService); @@ -77,25 +74,28 @@ async function bootstrap() { const jwtAuthenticator = new JwtAuthenticator(SECRET_KEY); - const server = new Server({ - port: baseConfig.port, - jwtSecret: SECRET_KEY, - cors: baseConfig.cors, - }, { - s3Service, - userService, - spotifyTokenService, - spotifyPollingService, - weatherPollingService, - jwtAuthenticator - }); + const server = new Server( + { + port: baseConfig.port, + jwtSecret: SECRET_KEY, + cors: baseConfig.cors, + }, + { + s3Service, + userService, + spotifyTokenService, + spotifyPollingService, + weatherPollingService, + jwtAuthenticator, + } + ); await server.start(); } -if (process.env.NODE_ENV !== 'test') { - bootstrap().catch(error => { +if (process.env.NODE_ENV !== "test") { + bootstrap().catch((error) => { console.error("Fatal error during server startup:", error.message); process.exit(1); }); -} \ No newline at end of file +} diff --git a/src/interfaces/CurrentlyPlaying.ts b/src/interfaces/CurrentlyPlaying.ts index 62e0f64..4744ffd 100644 --- a/src/interfaces/CurrentlyPlaying.ts +++ b/src/interfaces/CurrentlyPlaying.ts @@ -18,4 +18,4 @@ export interface CurrentlyPlaying { duration_ms: number; }; is_playing: boolean; -} \ No newline at end of file +} diff --git a/src/interfaces/decodedToken.ts b/src/interfaces/decodedToken.ts index e3e44bf..9992a93 100644 --- a/src/interfaces/decodedToken.ts +++ b/src/interfaces/decodedToken.ts @@ -1,5 +1,5 @@ export interface DecodedToken { - username: string; - id: string; - uuid: string; + username: string; + id: string; + uuid: string; } diff --git a/src/interfaces/extendedIncomingMessage.ts b/src/interfaces/extendedIncomingMessage.ts index 98d76a5..c7c380a 100644 --- a/src/interfaces/extendedIncomingMessage.ts +++ b/src/interfaces/extendedIncomingMessage.ts @@ -2,5 +2,5 @@ import { IncomingMessage } from "node:http"; import { DecodedToken } from "./decodedToken"; export interface ExtendedIncomingMessage extends IncomingMessage { - payload: DecodedToken; + payload: DecodedToken; } diff --git a/src/interfaces/extendedWebsocket.ts b/src/interfaces/extendedWebsocket.ts index f653644..fa6e8df 100644 --- a/src/interfaces/extendedWebsocket.ts +++ b/src/interfaces/extendedWebsocket.ts @@ -1,6 +1,6 @@ -import {WebSocket} from "ws"; -import {DecodedToken} from "./decodedToken"; -import {IUser} from "../db/models/user"; +import { WebSocket } from "ws"; +import { DecodedToken } from "./decodedToken"; +import { IUser } from "../db/models/user"; export interface ExtendedWebSocket extends WebSocket { payload: DecodedToken; diff --git a/src/rest/auth.ts b/src/rest/auth.ts index 8853ff5..d3d8c36 100644 --- a/src/rest/auth.ts +++ b/src/rest/auth.ts @@ -1,12 +1,12 @@ import express from "express"; -import {CreateUserPayload} from "../db/models/user"; -import {JwtAuthenticator} from "../utils/jwtAuthenticator"; +import { CreateUserPayload } from "../db/models/user"; +import { JwtAuthenticator } from "../utils/jwtAuthenticator"; import crypto from "crypto"; -import {PasswordUtils} from "../utils/passwordUtils"; -import {asyncHandler} from "./middleware/asyncHandler"; -import {validateBody, v} from "./middleware/validate"; -import {ok, badRequest, unauthorized, created, conflict, notFound} from "./utils/responses"; -import {UserService} from "../services/db/UserService"; +import { PasswordUtils } from "../utils/passwordUtils"; +import { asyncHandler } from "./middleware/asyncHandler"; +import { validateBody, v } from "./middleware/validate"; +import { ok, badRequest, unauthorized, created, conflict, notFound } from "./utils/responses"; +import { UserService } from "../services/db/UserService"; export class RestAuth { private readonly userService: UserService; @@ -23,25 +23,28 @@ export class RestAuth { router.post( "/register", validateBody({ - username: {required: true, validator: v.isString({nonEmpty: true, min: 3})}, - password: {required: true, validator: v.isString({nonEmpty: true, min: 8})}, - timezone: {required: true, validator: v.isString({nonEmpty: true})}, - location: {required: true, validator: v.isString({nonEmpty: true})}, + username: { required: true, validator: v.isString({ nonEmpty: true, min: 3 }) }, + password: { required: true, validator: v.isString({ nonEmpty: true, min: 8 }) }, + timezone: { required: true, validator: v.isString({ nonEmpty: true }) }, + location: { required: true, validator: v.isString({ nonEmpty: true }) }, }), asyncHandler(async (req, res) => { - const {username, password, timezone, location} = req.body as { - username: string; password: string; timezone: string; location: string; + const { username, password, timezone, location } = req.body as { + username: string; + password: string; + timezone: string; + location: string; }; if (await this.userService.existsUserByName(username)) { - return conflict(res, "Username already exists", {field: "username", code: "USERNAME_TAKEN"}); + return conflict(res, "Username already exists", { field: "username", code: "USERNAME_TAKEN" }); } const passwordValidation = PasswordUtils.validatePassword(password); if (!passwordValidation.valid) { return badRequest(res, passwordValidation.message ?? "Invalid password", { field: "password", - code: "INVALID_PASSWORD_FORMAT" + code: "INVALID_PASSWORD_FORMAT", }); } @@ -53,61 +56,61 @@ export class RestAuth { config: { isVisible: false, isAdmin: false, - canBeModified: false + canBeModified: false, }, timezone, - location + location, }; const result = await this.userService.createUser(newUser); - return created(res, {user: result}); + return created(res, { user: result }); }) ); router.post( "/login", validateBody({ - username: {required: true, validator: v.isString({nonEmpty: true})}, - password: {required: true, validator: v.isString({nonEmpty: true})}, + username: { required: true, validator: v.isString({ nonEmpty: true }) }, + password: { required: true, validator: v.isString({ nonEmpty: true }) }, }), asyncHandler(async (req, res) => { - const {username, password} = req.body as { username: string; password: string }; + const { username, password } = req.body as { username: string; password: string }; const user = await this.userService.getUserAuthByName(username); if (!user) { - return notFound(res, "User not found", {field: "username", code: "INVALID_USER"}); + return notFound(res, "User not found", { field: "username", code: "INVALID_USER" }); } const isValid = await PasswordUtils.comparePassword(password, user.password!); if (!isValid) { - return unauthorized(res, "Invalid password", {field: "password", code: "INVALID_PASSWORD"}); + return unauthorized(res, "Invalid password", { field: "password", code: "INVALID_PASSWORD" }); } const jwtToken = this.jwtAuthenticator.generateToken({ - username: user.name, - id: user.id, - uuid: user.uuid - }); - - res.cookie('auth-token', jwtToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - maxAge: 24 * 60 * 60 * 1000 + username: user.name, + id: user.id, + uuid: user.uuid, }); - return ok(res, {token: jwtToken}); + res.cookie("auth-token", jwtToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 24 * 60 * 60 * 1000, + }); + + return ok(res, { token: jwtToken }); }) ); router.post( "/logout", asyncHandler(async (req, res) => { - res.clearCookie('auth-token'); - return ok(res, {message: "Successfully logged out"}); + res.clearCookie("auth-token"); + return ok(res, { message: "Successfully logged out" }); }) ); return router; } -} \ No newline at end of file +} diff --git a/src/rest/jwtTokenPropertiesExtractor.ts b/src/rest/jwtTokenPropertiesExtractor.ts index 7605c1a..da1a9ee 100644 --- a/src/rest/jwtTokenPropertiesExtractor.ts +++ b/src/rest/jwtTokenPropertiesExtractor.ts @@ -7,19 +7,27 @@ export class JwtTokenPropertiesExtractor { public createRouter() { const router = express.Router(); - router.get("/id", asyncHandler(async (req: Request, res: Response) => { - return ok(res, req.payload.id); - })); + router.get( + "/id", + asyncHandler(async (req: Request, res: Response) => { + return ok(res, req.payload.id); + }) + ); + router.get( + "/username", + asyncHandler(async (req: Request, res: Response) => { + return ok(res, req.payload.username); + }) + ); - router.get("/username", asyncHandler(async (req: Request, res: Response) => { - return ok(res, req.payload.username); - })); - - router.get("/uuid", asyncHandler(async (req: Request, res: Response) => { - return ok(res, req.payload.uuid); - })); + router.get( + "/uuid", + asyncHandler(async (req: Request, res: Response) => { + return ok(res, req.payload.uuid); + }) + ); return router; } -} \ No newline at end of file +} diff --git a/src/rest/middleware/asyncHandler.ts b/src/rest/middleware/asyncHandler.ts index dacddcb..2a813cd 100644 --- a/src/rest/middleware/asyncHandler.ts +++ b/src/rest/middleware/asyncHandler.ts @@ -1,8 +1,6 @@ import type { Request, Response, NextFunction, RequestHandler } from "express"; -export function asyncHandler( - fn: (req: Request, res: Response, next: NextFunction) => Promise -): RequestHandler { +export function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise): RequestHandler { return async (req, res, next) => { try { await fn(req, res, next); diff --git a/src/rest/middleware/authenticateJwt.ts b/src/rest/middleware/authenticateJwt.ts index 9ab06c9..a3299c1 100644 --- a/src/rest/middleware/authenticateJwt.ts +++ b/src/rest/middleware/authenticateJwt.ts @@ -9,10 +9,7 @@ export function authenticateJwt(jwtAuthenticator: JwtAuthenticator) { const authHeader = req.headers["authorization"]; if (!authHeader) { - return unauthorized( - res, - "Unauthorized: No Authorization header provided" - ); + return unauthorized(res, "Unauthorized: No Authorization header provided"); } if (!authHeader.startsWith(BEARER_PREFIX)) { diff --git a/src/rest/middleware/extractTokenFromCookie.ts b/src/rest/middleware/extractTokenFromCookie.ts index 0b22f68..0a6d9ed 100644 --- a/src/rest/middleware/extractTokenFromCookie.ts +++ b/src/rest/middleware/extractTokenFromCookie.ts @@ -1,15 +1,15 @@ -import { Request, Response, NextFunction } from 'express'; +import { Request, Response, NextFunction } from "express"; export const extractTokenFromCookie = (req: Request, res: Response, next: NextFunction) => { if (req.headers.authorization) { return next(); } - const token = req.cookies['auth-token']; + const token = req.cookies["auth-token"]; if (token) { req.headers.authorization = `Bearer ${token}`; } next(); -}; \ No newline at end of file +}; diff --git a/src/rest/middleware/isAdmin.ts b/src/rest/middleware/isAdmin.ts index 47d70bb..9cba066 100644 --- a/src/rest/middleware/isAdmin.ts +++ b/src/rest/middleware/isAdmin.ts @@ -1,6 +1,6 @@ import type { NextFunction, Request, Response } from "express"; import { notFound } from "../utils/responses"; -import {UserService} from "../../services/db/UserService"; +import { UserService } from "../../services/db/UserService"; export function isAdmin(userService: UserService) { return async (req: Request, res: Response, next: NextFunction) => { @@ -17,4 +17,4 @@ export function isAdmin(userService: UserService) { return next(error); } }; -} \ No newline at end of file +} diff --git a/src/rest/middleware/rateLimit.ts b/src/rest/middleware/rateLimit.ts index 3cfc9e3..3c47520 100644 --- a/src/rest/middleware/rateLimit.ts +++ b/src/rest/middleware/rateLimit.ts @@ -20,4 +20,4 @@ export const spotifyLimiter = rateLimit({ standardHeaders: true, legacyHeaders: false, handler: onLimitReached, -}); \ No newline at end of file +}); diff --git a/src/rest/middleware/validate.ts b/src/rest/middleware/validate.ts index 1f47a15..d1cc19b 100644 --- a/src/rest/middleware/validate.ts +++ b/src/rest/middleware/validate.ts @@ -1,5 +1,5 @@ import type { Request, Response, NextFunction } from "express"; -import {badRequest} from "../utils/responses"; +import { badRequest } from "../utils/responses"; /** * A type definition for a validation function. @@ -39,51 +39,52 @@ type Validator = (value: any) => true | string; * Validates whether a value is a valid URL. The value must be a string and conform to standard URL formatting rules. */ export const v = { - isString: (opts?: { nonEmpty?: boolean; max?: number; min?: number }): Validator => { - return (value: any) => { - if (typeof value !== "string") return "must be a string"; - if (opts?.nonEmpty && value.trim().length === 0) return "must be a non-empty string"; - if (opts?.max !== undefined && value.length > opts.max) return `must be at most ${opts.max} chars`; - if (opts?.min !== undefined && value.length < opts.min) return `must be at least ${opts.min} chars`; - return true; - }; - }, - isNumber: (opts?: { min?: number; max?: number; integer?: boolean }): Validator => { - return (value: any) => { - if (typeof value !== "number" || Number.isNaN(value)) return "must be a number"; - if (opts?.integer && !Number.isInteger(value)) return "must be an integer"; - if (opts?.min !== undefined && value < opts.min) return `must be >= ${opts.min}`; - if (opts?.max !== undefined && value > opts.max) return `must be <= ${opts.max}`; - return true; - }; - }, - isBoolean: (): Validator => { - return (value: any) => (typeof value === "boolean" ? true : "must be a boolean"); - }, - isEnum: (values: T): Validator => { - return (value: any) => (values.includes(value) ? true : `must be one of: ${values.join(", ")}`); - }, - isArrayLength: (len: number): Validator => { - return (value: any) => (Array.isArray(value) && value.length === len ? true : `must be an array of length ${len}`); - }, - isObject: (opts?: { nonEmpty?: boolean }): Validator => { - return (value: any) => { - if (typeof value !== "object" || value === null) return "must be an object"; - if (opts?.nonEmpty && Object.keys(value).length === 0) return "must be a non-empty object"; - return true; - }; - }, - isUrl: (): Validator => { - return (value: any) => { - if (typeof value !== "string") return "must be a string URL"; - try { - new URL(value); - return true; - } catch { - return "must be a valid URL"; - } - }; - }, + isString: (opts?: { nonEmpty?: boolean; max?: number; min?: number }): Validator => { + return (value: any) => { + if (typeof value !== "string") return "must be a string"; + if (opts?.nonEmpty && value.trim().length === 0) return "must be a non-empty string"; + if (opts?.max !== undefined && value.length > opts.max) return `must be at most ${opts.max} chars`; + if (opts?.min !== undefined && value.length < opts.min) return `must be at least ${opts.min} chars`; + return true; + }; + }, + isNumber: (opts?: { min?: number; max?: number; integer?: boolean }): Validator => { + return (value: any) => { + if (typeof value !== "number" || Number.isNaN(value)) return "must be a number"; + if (opts?.integer && !Number.isInteger(value)) return "must be an integer"; + if (opts?.min !== undefined && value < opts.min) return `must be >= ${opts.min}`; + if (opts?.max !== undefined && value > opts.max) return `must be <= ${opts.max}`; + return true; + }; + }, + isBoolean: (): Validator => { + return (value: any) => (typeof value === "boolean" ? true : "must be a boolean"); + }, + isEnum: (values: T): Validator => { + return (value: any) => (values.includes(value) ? true : `must be one of: ${values.join(", ")}`); + }, + isArrayLength: (len: number): Validator => { + return (value: any) => + Array.isArray(value) && value.length === len ? true : `must be an array of length ${len}`; + }, + isObject: (opts?: { nonEmpty?: boolean }): Validator => { + return (value: any) => { + if (typeof value !== "object" || value === null) return "must be an object"; + if (opts?.nonEmpty && Object.keys(value).length === 0) return "must be a non-empty object"; + return true; + }; + }, + isUrl: (): Validator => { + return (value: any) => { + if (typeof value !== "string") return "must be a string URL"; + try { + new URL(value); + return true; + } catch { + return "must be a valid URL"; + } + }; + }, }; /** @@ -106,17 +107,17 @@ type Schema = Record; * @return {string[]} An array of error messages. If there are no validation errors, the array will be empty. */ function validate(source: any, schema: Schema): string[] { - const errors: string[] = []; - for (const [key, rule] of Object.entries(schema)) { - const value = source?.[key]; - if (value === undefined || value === null) { - if (rule.required) errors.push(`${key} is required`); - continue; + const errors: string[] = []; + for (const [key, rule] of Object.entries(schema)) { + const value = source?.[key]; + if (value === undefined || value === null) { + if (rule.required) errors.push(`${key} is required`); + continue; + } + const res = rule.validator(value); + if (res !== true) errors.push(`${key} ${res}`); } - const res = rule.validator(value); - if (res !== true) errors.push(`${key} ${res}`); - } - return errors; + return errors; } /** @@ -126,11 +127,11 @@ function validate(source: any, schema: Schema): string[] { * @return {Function} Express middleware function that validates the request body and invokes the next middleware if valid; otherwise, it responds with a 400 status and validation errors. */ export function validateBody(schema: Schema) { - return (req: Request, res: Response, next: NextFunction) => { - const errs = validate(req.body, schema); - if (errs.length) return badRequest(res, "Validation failed", errs); - next(); - }; + return (req: Request, res: Response, next: NextFunction) => { + const errs = validate(req.body, schema); + if (errs.length) return badRequest(res, "Validation failed", errs); + next(); + }; } /** @@ -140,11 +141,11 @@ export function validateBody(schema: Schema) { * @return {(req: Request, res: Response, next: NextFunction) => void} A middleware function that validates the request parameters. */ export function validateParams(schema: Schema) { - return (req: Request, res: Response, next: NextFunction) => { - const errs = validate(req.params, schema); - if (errs.length) return res.status(400).send({ error: "Validation failed", details: errs }); - next(); - }; + return (req: Request, res: Response, next: NextFunction) => { + const errs = validate(req.params, schema); + if (errs.length) return res.status(400).send({ error: "Validation failed", details: errs }); + next(); + }; } /** @@ -154,9 +155,9 @@ export function validateParams(schema: Schema) { * @return {Function} Middleware function that validates the query parameters and either sends a 400 response with validation errors or proceeds to the next middleware. */ export function validateQuery(schema: Schema) { - return (req: Request, res: Response, next: NextFunction) => { - const errs = validate(req.query, schema); - if (errs.length) return res.status(400).send({ error: "Validation failed", details: errs }); - next(); - }; -} \ No newline at end of file + return (req: Request, res: Response, next: NextFunction) => { + const errs = validate(req.query, schema); + if (errs.length) return res.status(400).send({ error: "Validation failed", details: errs }); + next(); + }; +} diff --git a/src/rest/restStorage.ts b/src/rest/restStorage.ts index 7ea9e0f..d55b0ad 100644 --- a/src/rest/restStorage.ts +++ b/src/rest/restStorage.ts @@ -1,32 +1,30 @@ -import {S3Service} from "../services/s3Service"; -import multer from "multer" +import { S3Service } from "../services/s3Service"; +import multer from "multer"; import express from "express"; -import {asyncHandler} from "./middleware/asyncHandler"; -import {badRequest, created, forbidden, notFound, ok} from "./utils/responses"; -import {vi} from "vitest"; +import { asyncHandler } from "./middleware/asyncHandler"; +import { badRequest, created, forbidden, notFound, ok } from "./utils/responses"; +import { vi } from "vitest"; vi.mock("../../src/services/db/UserService", () => ({ UserService: { - create: vi.fn() - } + create: vi.fn(), + }, })); export class RestStorage { - - constructor(private readonly s3Service: S3Service) { - } + constructor(private readonly s3Service: S3Service) {} public createRouter() { const router = express.Router(); const upload = multer({ storage: multer.memoryStorage(), - limits: {fileSize: 10 * 1024 * 1024}, + limits: { fileSize: 10 * 1024 * 1024 }, }); router.post( "/upload", - upload.single('image'), + upload.single("image"), asyncHandler(async (req, res) => { if (!req.file) { return badRequest(res, "No file provided."); @@ -35,56 +33,65 @@ export class RestStorage { const userId = req.payload.uuid; const objectKey = await this.s3Service.uploadFile(req.file, userId); - return created(res, {message: "File uploaded successfully", objectKey}) + return created(res, { message: "File uploaded successfully", objectKey }); }) ); - router.get("/files", asyncHandler(async (req, res) => { - const userId = req.payload.uuid; - const files = await this.s3Service.listFilesForUser(userId); + router.get( + "/files", + asyncHandler(async (req, res) => { + const userId = req.payload.uuid; + const files = await this.s3Service.listFilesForUser(userId); - return ok(res, files); - })); + return ok(res, files); + }) + ); - router.get(/\/files\/(.*)\/url$/, asyncHandler(async (req, res) => { - const userId = req.payload.uuid; - const objectKey =req.params[0]; + router.get( + /\/files\/(.*)\/url$/, + asyncHandler(async (req, res) => { + const userId = req.payload.uuid; + const objectKey = req.params[0]; - console.log(userId); - console.log(objectKey) + console.log(userId); + console.log(objectKey); - if (!objectKey || !objectKey.startsWith(`user-${userId}`)) { - return forbidden(res); - } - try { - const expiresInSeconds = 60; - const downloadUrl = await this.s3Service.getSignedDownloadUrl(objectKey, expiresInSeconds); - - return ok(res, {url: downloadUrl}); - } catch (error: any) { - if (error.name === "NoSuchKey") { - return notFound(res, "File not found."); - } else { - throw error; + if (!objectKey || !objectKey.startsWith(`user-${userId}`)) { + return forbidden(res); } - } + try { + const expiresInSeconds = 60; + const downloadUrl = await this.s3Service.getSignedDownloadUrl(objectKey, expiresInSeconds); - })); + return ok(res, { url: downloadUrl }); + } catch (error: any) { + if (error.name === "NoSuchKey") { + return notFound(res, "File not found."); + } else { + throw error; + } + } + }) + ); - router.delete(/\/files\/(.*)/, asyncHandler(async (req, res) => { // <-- ÄNDERUNG HIER - const userId = req.payload.uuid; - const objectKey =req.params[0]; + router.delete( + /\/files\/(.*)/, + asyncHandler(async (req, res) => { + // <-- ÄNDERUNG HIER + const userId = req.payload.uuid; + const objectKey = req.params[0]; - console.log(objectKey) - if (!objectKey.startsWith(`user-${userId}/`)) { - return forbidden(res); - } + console.log(objectKey); + if (!objectKey.startsWith(`user-${userId}/`)) { + return forbidden(res); + } - await this.s3Service.deleteFile(objectKey); + await this.s3Service.deleteFile(objectKey); - return ok(res, "File deleted successfully"); - })); + return ok(res, "File deleted successfully"); + }) + ); return router; } -} \ No newline at end of file +} diff --git a/src/rest/restUser.ts b/src/rest/restUser.ts index f6405d5..131a483 100644 --- a/src/rest/restUser.ts +++ b/src/rest/restUser.ts @@ -1,13 +1,12 @@ import express from "express"; -import {PasswordUtils} from "../utils/passwordUtils"; -import {asyncHandler} from "./middleware/asyncHandler"; -import {v, validateBody, validateParams} from "./middleware/validate"; -import {badRequest, ok} from "./utils/responses"; -import {isAdmin} from "./middleware/isAdmin"; -import {UserService} from "../services/db/UserService"; +import { PasswordUtils } from "../utils/passwordUtils"; +import { asyncHandler } from "./middleware/asyncHandler"; +import { v, validateBody, validateParams } from "./middleware/validate"; +import { badRequest, ok } from "./utils/responses"; +import { isAdmin } from "./middleware/isAdmin"; +import { UserService } from "../services/db/UserService"; export class RestUser { - private readonly userService: UserService; constructor(userService: UserService) { @@ -17,27 +16,37 @@ export class RestUser { public createRouter() { const router = express.Router(); - router.get("/", isAdmin(this.userService), asyncHandler(async (_req, res) => { - const users = await this.userService.getAllUsers(); - return ok(res, {users}); - })); + router.get( + "/", + isAdmin(this.userService), + asyncHandler(async (_req, res) => { + const users = await this.userService.getAllUsers(); + return ok(res, { users }); + }) + ); - router.get("/me", asyncHandler(async (req, res) => { - const user = await this.userService.getUserByUUID(req.payload.uuid); - return ok(res, user); - })); + router.get( + "/me", + asyncHandler(async (req, res) => { + const user = await this.userService.getUserByUUID(req.payload.uuid); + return ok(res, user); + }) + ); router.put( "/me/spotify", validateBody({ - accessToken: {required: true, validator: v.isString({nonEmpty: true})}, - refreshToken: {required: true, validator: v.isString({nonEmpty: true})}, - scope: {required: true, validator: v.isString({nonEmpty: true})}, - expirationDate: {required: true, validator: v.isString({nonEmpty: true})}, + accessToken: { required: true, validator: v.isString({ nonEmpty: true }) }, + refreshToken: { required: true, validator: v.isString({ nonEmpty: true }) }, + scope: { required: true, validator: v.isString({ nonEmpty: true }) }, + expirationDate: { required: true, validator: v.isString({ nonEmpty: true }) }, }), asyncHandler(async (req, res) => { - const {accessToken, refreshToken, scope, expirationDate} = req.body as { - accessToken: string; refreshToken: string; scope: string; expirationDate: string; + const { accessToken, refreshToken, scope, expirationDate } = req.body as { + accessToken: string; + refreshToken: string; + scope: string; + expirationDate: string; }; const spotifyConfig = { @@ -47,24 +56,30 @@ export class RestUser { expirationDate: new Date(expirationDate), }; - await this.userService.updateUserByUUID(req.payload.uuid, {spotifyConfig: spotifyConfig}); - return ok(res, {message: "Spotify config changed successfully."}); + await this.userService.updateUserByUUID(req.payload.uuid, { spotifyConfig: spotifyConfig }); + return ok(res, { message: "Spotify config changed successfully." }); }) ); - router.delete("/me/spotify", asyncHandler(async (req, res) => { - const updated = await this.userService.clearSpotifyConfigByUUID(req.payload.uuid); - return ok(res, {user: updated}); - })); + router.delete( + "/me/spotify", + asyncHandler(async (req, res) => { + const updated = await this.userService.clearSpotifyConfigByUUID(req.payload.uuid); + return ok(res, { user: updated }); + }) + ); router.put( "/me/password", validateBody({ - password: {required: true, validator: v.isString({nonEmpty: true, min: 8})}, - passwordConfirmation: {required: true, validator: v.isString({nonEmpty: true, min: 8})}, + password: { required: true, validator: v.isString({ nonEmpty: true, min: 8 }) }, + passwordConfirmation: { required: true, validator: v.isString({ nonEmpty: true, min: 8 }) }, }), asyncHandler(async (req, res) => { - const {password, passwordConfirmation} = req.body as { password: string; passwordConfirmation: string }; + const { password, passwordConfirmation } = req.body as { + password: string; + passwordConfirmation: string; + }; if (password !== passwordConfirmation) { return badRequest(res, "Passwörter stimmen nicht überein"); @@ -77,15 +92,15 @@ export class RestUser { const newPassword = await PasswordUtils.hashPassword(password); - await this.userService.updateUserByUUID(req.payload.uuid, {password: newPassword}); - return ok(res, {message: "Password changed successfully"}); + await this.userService.updateUserByUUID(req.payload.uuid, { password: newPassword }); + return ok(res, { message: "Password changed successfully" }); }) ); router.get( "/:id", validateParams({ - id: {required: true, validator: v.isString({nonEmpty: true})}, + id: { required: true, validator: v.isString({ nonEmpty: true }) }, }), isAdmin(this.userService), asyncHandler(async (req, res) => { @@ -101,4 +116,4 @@ export class RestUser { return router; } -} \ No newline at end of file +} diff --git a/src/rest/restWebSocket.ts b/src/rest/restWebSocket.ts index ded5b8d..ab77b99 100644 --- a/src/rest/restWebSocket.ts +++ b/src/rest/restWebSocket.ts @@ -1,9 +1,9 @@ import express, { Router, Request, Response } from "express"; import { ExtendedWebSocketServer } from "../websocket"; import { asyncHandler } from "./middleware/asyncHandler"; -import {v, validateBody} from "./middleware/validate"; +import { v, validateBody } from "./middleware/validate"; import { ok } from "./utils/responses"; -import {ExtendedWebSocket} from "../interfaces/extendedWebsocket"; +import { ExtendedWebSocket } from "../interfaces/extendedWebsocket"; export class RestWebSocket { constructor(private webSocketServer: ExtendedWebSocketServer) {} @@ -36,7 +36,9 @@ export class RestWebSocket { users: { required: true, validator: (value: any) => - Array.isArray(value) && value.length > 0 && value.every((s) => typeof s === "string" && s.trim().length > 0) + Array.isArray(value) && + value.length > 0 && + value.every((s) => typeof s === "string" && s.trim().length > 0) ? true : "must be a non-empty array of strings", }, @@ -51,13 +53,15 @@ export class RestWebSocket { }) ); - router.get("/all-clients", asyncHandler(async (_req: Request, res: Response) => { - const connectedClients = this.webSocketServer.getConnectedClients(); - const result = Array.from(connectedClients).map((client: ExtendedWebSocket) => client.payload); - return ok(res, { result }); - })); - + router.get( + "/all-clients", + asyncHandler(async (_req: Request, res: Response) => { + const connectedClients = this.webSocketServer.getConnectedClients(); + const result = Array.from(connectedClients).map((client: ExtendedWebSocket) => client.payload); + return ok(res, { result }); + }) + ); return router; } -} \ No newline at end of file +} diff --git a/src/rest/spotifyTokenGenerator.ts b/src/rest/spotifyTokenGenerator.ts index de334d0..b6887ea 100644 --- a/src/rest/spotifyTokenGenerator.ts +++ b/src/rest/spotifyTokenGenerator.ts @@ -1,13 +1,11 @@ import express from "express"; -import {asyncHandler} from "./middleware/asyncHandler"; -import {validateBody, v} from "./middleware/validate"; -import {ok, internalError} from "./utils/responses"; -import {SpotifyTokenService} from "../services/spotifyTokenService"; +import { asyncHandler } from "./middleware/asyncHandler"; +import { validateBody, v } from "./middleware/validate"; +import { ok, internalError } from "./utils/responses"; +import { SpotifyTokenService } from "../services/spotifyTokenService"; export class SpotifyTokenGenerator { - - constructor(private spotifyTokenService: SpotifyTokenService) { - } + constructor(private spotifyTokenService: SpotifyTokenService) {} public createRouter() { const router = express.Router(); @@ -15,29 +13,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.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.spotifyTokenService.generateToken(authCode, redirectUri); - return ok(res, {token}); + return ok(res, { token }); }) ); @@ -47,4 +45,4 @@ export class SpotifyTokenGenerator { return router; } -} \ No newline at end of file +} diff --git a/src/rest/utils/responses.ts b/src/rest/utils/responses.ts index 28ec3a6..ddece11 100644 --- a/src/rest/utils/responses.ts +++ b/src/rest/utils/responses.ts @@ -2,39 +2,34 @@ import type { Response } from "express"; type ErrorDetails = unknown; -function respondError( - res: Response, - status: number, - message: string, - details?: ErrorDetails -) { - return res.status(status).send({ - ok: false, - data: { - message, - details, - }, - }); +function respondError(res: Response, status: number, message: string, details?: ErrorDetails) { + return res.status(status).send({ + ok: false, + data: { + message, + details, + }, + }); } export function ok(res: Response, data: T) { - return res.status(200).send({ ok: true, data }); + return res.status(200).send({ ok: true, data }); } export function created(res: Response, data: T) { - return res.status(201).send({ ok: true, data }); + return res.status(201).send({ ok: true, data }); } export function badRequest(res: Response, message = "Bad Request", details?: ErrorDetails) { - return respondError(res, 400, message, details); + return respondError(res, 400, message, details); } export function unauthorized(res: Response, message = "Unauthorized", details?: ErrorDetails) { - return respondError(res, 401, message, details); + return respondError(res, 401, message, details); } export function forbidden(res: Response, message = "Forbidden", details?: ErrorDetails) { - return respondError(res, 403, message, details); + return respondError(res, 403, message, details); } export function notFound(res: Response, message = "Not Found", details?: ErrorDetails) { @@ -42,13 +37,13 @@ export function notFound(res: Response, message = "Not Found", details?: ErrorDe } export function conflict(res: Response, message = "Conflict", details?: ErrorDetails) { - return respondError(res, 409, message, details); + return respondError(res, 409, message, details); } export function tooManyRequests(res: Response, message = "Too Many Requests", details?: ErrorDetails) { - return respondError(res, 429, message, details); + return respondError(res, 429, message, details); } export function internalError(res: Response, message = "Internal Server Error", details?: ErrorDetails) { - return respondError(res, 500, message, details); + return respondError(res, 500, message, details); } diff --git a/src/server.ts b/src/server.ts index 6fdd24c..974a7e4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,27 +1,27 @@ -import express, {Express, Request, Response, NextFunction} from "express"; -import {Server as HttpServer} from "http"; +import express, { Express, Request, Response, NextFunction } from "express"; +import { Server as HttpServer } from "http"; import cors from "cors"; -import cookieParser from 'cookie-parser'; -import {randomUUID} from "crypto"; +import cookieParser from "cookie-parser"; +import { randomUUID } from "crypto"; -import {ExtendedWebSocketServer} from "./websocket"; -import {RestWebSocket} from "./rest/restWebSocket"; -import {RestUser} from "./rest/restUser"; -import {JwtTokenPropertiesExtractor} from "./rest/jwtTokenPropertiesExtractor"; -import {SpotifyTokenGenerator} from "./rest/spotifyTokenGenerator"; -import {RestAuth} from "./rest/auth"; -import {authLimiter, spotifyLimiter} from "./rest/middleware/rateLimit"; -import {extractTokenFromCookie} from "./rest/middleware/extractTokenFromCookie"; -import {JwtAuthenticator} from "./utils/jwtAuthenticator"; -import {authenticateJwt} from "./rest/middleware/authenticateJwt"; -import {watchUserChanges} from "./db/models/userWatch"; -import {SpotifyPollingService} from "./services/spotifyPollingService"; -import {UserService} from "./services/db/UserService"; -import {disconnectFromDatabase} from "./services/db/database.service"; -import {SpotifyTokenService} from "./services/spotifyTokenService"; -import {WeatherPollingService} from "./services/weatherPollingService"; -import {S3Service} from "./services/s3Service"; -import {RestStorage} from "./rest/restStorage"; +import { ExtendedWebSocketServer } from "./websocket"; +import { RestWebSocket } from "./rest/restWebSocket"; +import { RestUser } from "./rest/restUser"; +import { JwtTokenPropertiesExtractor } from "./rest/jwtTokenPropertiesExtractor"; +import { SpotifyTokenGenerator } from "./rest/spotifyTokenGenerator"; +import { RestAuth } from "./rest/auth"; +import { authLimiter, spotifyLimiter } from "./rest/middleware/rateLimit"; +import { extractTokenFromCookie } from "./rest/middleware/extractTokenFromCookie"; +import { JwtAuthenticator } from "./utils/jwtAuthenticator"; +import { authenticateJwt } from "./rest/middleware/authenticateJwt"; +import { watchUserChanges } from "./db/models/userWatch"; +import { SpotifyPollingService } from "./services/spotifyPollingService"; +import { UserService } from "./services/db/UserService"; +import { disconnectFromDatabase } from "./services/db/database.service"; +import { SpotifyTokenService } from "./services/spotifyTokenService"; +import { WeatherPollingService } from "./services/weatherPollingService"; +import { S3Service } from "./services/s3Service"; +import { RestStorage } from "./rest/restStorage"; interface ServerDependencies { userService: UserService; @@ -46,8 +46,10 @@ export class Server { private httpServer: HttpServer | null = null; private webSocketServer: ExtendedWebSocketServer | null = null; - constructor(private readonly config: ServerConfig, - private readonly dependencies: ServerDependencies) { + constructor( + private readonly config: ServerConfig, + private readonly dependencies: ServerDependencies + ) { this.app = express(); } @@ -58,10 +60,10 @@ export class Server { spotifyTokenService, spotifyPollingService, weatherPollingService, - jwtAuthenticator + jwtAuthenticator, } = this.dependencies; - await s3Service.ensureBucketExists() + await s3Service.ensureBucketExists(); watchUserChanges(); @@ -73,7 +75,13 @@ export class Server { console.log(`Server is running on port ${this.config.port}`); }); - this.webSocketServer = new ExtendedWebSocketServer(this.httpServer, userService, spotifyPollingService, weatherPollingService, jwtAuthenticator); + this.webSocketServer = new ExtendedWebSocketServer( + this.httpServer, + userService, + spotifyPollingService, + weatherPollingService, + jwtAuthenticator + ); this._setupGracefulShutdown(); @@ -93,15 +101,22 @@ export class Server { private _setupMiddleware(): void { this.app.set("trust proxy", 1); this.app.use(cookieParser()); - this.app.use(cors({ - origin: this.config.cors.origin, - credentials: this.config.cors.credentials, - })); + this.app.use( + cors({ + origin: this.config.cors.origin, + credentials: this.config.cors.credentials, + }) + ); this.app.use(this._securityHeaders); - this.app.use(express.json({limit: "2mb"})); + this.app.use(express.json({ limit: "2mb" })); } - private _setupRoutes(userService: UserService, spotifyTokenService: SpotifyTokenService, jwtAuthenticator: JwtAuthenticator, s3Service: S3Service): void { + private _setupRoutes( + userService: UserService, + spotifyTokenService: SpotifyTokenService, + jwtAuthenticator: JwtAuthenticator, + s3Service: S3Service + ): void { const _authenticateJwt = authenticateJwt(jwtAuthenticator); const restAuth = new RestAuth(userService, jwtAuthenticator); @@ -110,7 +125,7 @@ export class Server { const jwtTokenExtractor = new JwtTokenPropertiesExtractor(); const storage = new RestStorage(s3Service); - this.app.get("/api/healthz", (_req, res) => res.status(200).send({status: "ok"})); + this.app.get("/api/healthz", (_req, res) => res.status(200).send({ status: "ok" })); this.app.use("/api/auth", authLimiter, restAuth.createRouter()); @@ -160,7 +175,7 @@ export class Server { ok: false, data: { error: errorMessage, - ...(statusCode >= 500 && {errorId: errorId}), + ...(statusCode >= 500 && { errorId: errorId }), }, }); }); @@ -173,4 +188,4 @@ export class Server { process.exit(0); }); } -} \ No newline at end of file +} diff --git a/src/services/db/UserService.ts b/src/services/db/UserService.ts index f126b80..875e651 100644 --- a/src/services/db/UserService.ts +++ b/src/services/db/UserService.ts @@ -1,12 +1,11 @@ -import {connectToDatabase} from "./database.service"; -import { UpdateQuery} from "mongoose"; -import {CreateUserPayload, IUser, SpotifyConfig, UserModel} from "../../db/models/user"; +import { connectToDatabase } from "./database.service"; +import { UpdateQuery } from "mongoose"; +import { CreateUserPayload, IUser, SpotifyConfig, UserModel } from "../../db/models/user"; export class UserService { private static _instance: UserService; - private constructor() { - } + private constructor() {} public static async create(): Promise { if (!this._instance) { @@ -23,15 +22,11 @@ export class UserService { } public async updateUserByUUID(uuid: string, updates: Partial): Promise { - return await UserModel.findOneAndUpdate( - { uuid: uuid }, - { $set: updates }, - { new: true } - ).exec(); + return await UserModel.findOneAndUpdate({ uuid: uuid }, { $set: updates }, { new: true }).exec(); } public async getAllUsers(): Promise { - return await UserModel.find({}, {spotifyConfig: 0, lastState: 0}).exec(); + return await UserModel.find({}, { spotifyConfig: 0, lastState: 0 }).exec(); } public async getUserById(id: string): Promise { @@ -39,25 +34,21 @@ export class UserService { } public async getUserByUUID(uuid: string): Promise { - return await UserModel.findOne({uuid}).exec(); + return await UserModel.findOne({ uuid }).exec(); } public async getUserByName(name: string): Promise { - return await UserModel.findOne({name}) - .collation({locale: "en", strength: 2}) - .exec(); + return await UserModel.findOne({ name }).collation({ locale: "en", strength: 2 }).exec(); } public async getUserAuthByName(name: string): Promise { - return await UserModel.findOne({name}) - .collation({locale: "en", strength: 2}) - .select("+password") - .exec(); + return await UserModel.findOne({ name }).collation({ locale: "en", strength: 2 }).select("+password").exec(); } - public async getSpotifyConfigByUUID(uuid: string): Promise { - return await UserModel.findOne({uuid}, {spotifyConfig: 1}).exec().then(user => user?.spotifyConfig); + return await UserModel.findOne({ uuid }, { spotifyConfig: 1 }) + .exec() + .then((user) => user?.spotifyConfig); } public async createUser(userData: CreateUserPayload): Promise { @@ -68,13 +59,12 @@ export class UserService { delete userObject.password; return userObject as IUser; - } catch (error: any) { if (error.code === 11000 && error.keyPattern?.uuid) { throw new Error("User with that uuid already exists"); } - if (error.name === 'ValidationError') { + if (error.name === "ValidationError") { throw new Error(`ValidationError: ${error.message}`); } @@ -88,12 +78,8 @@ export class UserService { } public async clearSpotifyConfigByUUID(uuid: string): Promise { - return await UserModel.findOneAndUpdate( - { uuid }, - { $unset: { spotifyConfig: 1 } } as UpdateQuery, - { new: true } - ).exec(); + return await UserModel.findOneAndUpdate({ uuid }, { $unset: { spotifyConfig: 1 } } as UpdateQuery, { + new: true, + }).exec(); } - - } diff --git a/src/services/db/database.service.ts b/src/services/db/database.service.ts index 605b1cd..f134182 100644 --- a/src/services/db/database.service.ts +++ b/src/services/db/database.service.ts @@ -18,7 +18,7 @@ const connectWithRetry = async (dbName: string, dbConnString: string): Promise(resolve => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 5000)); return connectWithRetry(dbName, dbConnString); } }; @@ -34,19 +34,19 @@ export async function connectToDatabase(dbName: string, dbConnString: string): P return; } - mongoose.connection.on('connected', () => { + mongoose.connection.on("connected", () => { isConnected = true; - console.log('Mongoose connected to DB.'); + console.log("Mongoose connected to DB."); }); - mongoose.connection.on('disconnected', () => { + mongoose.connection.on("disconnected", () => { isConnected = false; - console.warn('Mongoose disconnected from DB. Attempting to reconnect...'); + console.warn("Mongoose disconnected from DB. Attempting to reconnect..."); }); - mongoose.connection.on('error', (err: Error) => { + mongoose.connection.on("error", (err: Error) => { isConnected = false; - console.error('Mongoose connection error:', err); + console.error("Mongoose connection error:", err); }); await connectWithRetry(dbName, dbConnString); diff --git a/src/services/owmApiService.ts b/src/services/owmApiService.ts index bb12f2e..2bd9423 100644 --- a/src/services/owmApiService.ts +++ b/src/services/owmApiService.ts @@ -1,7 +1,7 @@ -import OpenWeatherAPI from "openweather-api-node" +import OpenWeatherAPI from "openweather-api-node"; function getWeatherInstance(): OpenWeatherAPI { - return new OpenWeatherAPI({ + return new OpenWeatherAPI({ key: process.env.OWM_API_KEY, }); } @@ -9,6 +9,6 @@ function getWeatherInstance(): OpenWeatherAPI { export async function getCurrentWeather(location: string) { return getWeatherInstance().getCurrent({ locationName: location, - units: "metric" + units: "metric", }); } diff --git a/src/services/s3Service.ts b/src/services/s3Service.ts index 346d44f..385c1d1 100644 --- a/src/services/s3Service.ts +++ b/src/services/s3Service.ts @@ -2,10 +2,12 @@ import { S3Client, CreateBucketCommand, PutObjectCommand, - GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand + GetObjectCommand, + ListObjectsV2Command, + DeleteObjectCommand, } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { randomUUID } from 'crypto'; +import { randomUUID } from "crypto"; export interface S3ClientConfig { endpoint: string; @@ -51,7 +53,7 @@ export class S3Service { await this.client.send(new CreateBucketCommand({ Bucket: this.bucketName })); console.log(`Bucket "${this.bucketName}" created successfully or already existed.`); } catch (err: any) { - if (err.name === 'BucketAlreadyOwnedByYou' || err.name === 'BucketAlreadyExists') { + if (err.name === "BucketAlreadyOwnedByYou" || err.name === "BucketAlreadyExists") { console.log(`Bucket "${this.bucketName}" already exists.`); } else { throw err; @@ -60,7 +62,7 @@ export class S3Service { } async uploadFile(file: Express.Multer.File, userId: string): Promise { - const fileExtension = file.originalname.split('.').pop(); + const fileExtension = file.originalname.split(".").pop(); const objectKey = `user-${userId}/${randomUUID()}.${fileExtension}`; const command = new PutObjectCommand({ @@ -74,7 +76,7 @@ export class S3Service { return objectKey; } - async listFilesForUser(userId: string): Promise<{ key: string, lastModified: Date }[]> { + async listFilesForUser(userId: string): Promise<{ key: string; lastModified: Date }[]> { const command = new ListObjectsV2Command({ Bucket: this.bucketName, Prefix: `user-${userId}/`, @@ -82,10 +84,12 @@ export class S3Service { const response = await this.client.send(command); - return response.Contents?.map(item => ({ - key: item.Key!, - lastModified: item.LastModified!, - })) || []; + return ( + response.Contents?.map((item) => ({ + key: item.Key!, + lastModified: item.LastModified!, + })) || [] + ); } async deleteFile(objectKey: string): Promise { @@ -106,4 +110,4 @@ export class S3Service { return await getSignedUrl(this.client, command, { expiresIn }); } -} \ No newline at end of file +} diff --git a/src/services/spotifyApiService.ts b/src/services/spotifyApiService.ts index 08076a9..4f161d4 100644 --- a/src/services/spotifyApiService.ts +++ b/src/services/spotifyApiService.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import {CurrentlyPlaying} from "../interfaces/CurrentlyPlaying"; +import { CurrentlyPlaying } from "../interfaces/CurrentlyPlaying"; export class SpotifyApiService { private readonly apiUrl = "https://api.spotify.com/v1"; @@ -8,11 +8,11 @@ export class SpotifyApiService { try { const response = await axios.get(`${this.apiUrl}/me/player/currently-playing`, { headers: { - Authorization: `Bearer ${accessToken}` + Authorization: `Bearer ${accessToken}`, }, params: { - additional_types: "episode" - } + additional_types: "episode", + }, }); if (response.status === 204) { @@ -28,4 +28,4 @@ export class SpotifyApiService { throw error; } } -} \ No newline at end of file +} diff --git a/src/services/spotifyPollingService.ts b/src/services/spotifyPollingService.ts index dfe01e2..6f5e01c 100644 --- a/src/services/spotifyPollingService.ts +++ b/src/services/spotifyPollingService.ts @@ -2,8 +2,8 @@ import { appEventBus, SPOTIFY_STATE_UPDATED_EVENT } from "../utils/eventBus"; import { SpotifyApiService } from "./spotifyApiService"; import { IUser } from "../db/models/user"; import { AxiosError } from "axios"; -import {UserService} from "./db/UserService"; -import {SpotifyTokenService} from "./spotifyTokenService"; +import { UserService } from "./db/UserService"; +import { SpotifyTokenService } from "./spotifyTokenService"; const userStateCache = new Map(); const activePolls = new Map(); @@ -12,7 +12,7 @@ export class SpotifyPollingService { constructor( private readonly userService: UserService, private readonly spotifyApiService: SpotifyApiService, - private readonly spotifyTokenService: SpotifyTokenService, + private readonly spotifyTokenService: SpotifyTokenService ) {} public startPollingForUser(user: IUser): void { @@ -65,11 +65,10 @@ export class SpotifyPollingService { userStateCache.set(uuid, currentState); appEventBus.emit(SPOTIFY_STATE_UPDATED_EVENT, { uuid, state: currentState }); } - } catch (error) { if (error instanceof AxiosError && error.response) { if (error.response.status === 429) { - const retryAfter = Number(error.response.headers['retry-after'] || 5); + const retryAfter = Number(error.response.headers["retry-after"] || 5); console.warn(`[SpotifyPolling] Rate limit for ${uuid}. Pausing for ${retryAfter}s.`); this._pausePolling(uuid, retryAfter * 1000); } else if (error.response.status === 401) { @@ -86,8 +85,7 @@ export class SpotifyPollingService { if (!currentState && !lastState) return false; if (!currentState || !lastState) return true; - return lastState.item?.id !== currentState.item?.id || - lastState.is_playing !== currentState.is_playing; + return lastState.item?.id !== currentState.item?.id || lastState.is_playing !== currentState.is_playing; } private _pausePolling(uuid: string, durationMs: number): void { @@ -96,10 +94,10 @@ export class SpotifyPollingService { activePolls.delete(uuid); setTimeout(() => { console.log(`[SpotifyPolling] Resuming polling for ${uuid}.`); - this.userService.getUserByUUID(uuid).then(user => { + this.userService.getUserByUUID(uuid).then((user) => { if (user) this.startPollingForUser(user); }); }, durationMs); } } -} \ No newline at end of file +} diff --git a/src/services/spotifyTokenService.ts b/src/services/spotifyTokenService.ts index af38e16..47a0067 100644 --- a/src/services/spotifyTokenService.ts +++ b/src/services/spotifyTokenService.ts @@ -1,43 +1,37 @@ import axios from "axios"; -import {OAuthTokenResponse} from "../interfaces/OAuthTokenResponse"; +import { OAuthTokenResponse } from "../interfaces/OAuthTokenResponse"; const url = "https://accounts.spotify.com/api/token"; export class SpotifyTokenService { - constructor(private readonly clientId: string, private readonly clientSecret: string) { - } + constructor( + private readonly clientId: string, + private readonly clientSecret: string + ) {} public async refreshToken(refreshToken: string) { - console.log("refreshToken") - const response = await axios.post( - url, - `grant_type=refresh_token&refresh_token=${refreshToken}`, - { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Basic ${Buffer.from( - `${this.clientId}:${this.clientSecret}`, - ).toString("base64")}`, - }, + console.log("refreshToken"); + const response = await axios.post(url, `grant_type=refresh_token&refresh_token=${refreshToken}`, { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")}`, }, - ); + }); return response.data as OAuthTokenResponse; } public async generateToken(authorizationCode: string, redirectUri: string) { - console.log("generateToken") + console.log("generateToken"); const response = await axios.post( url, `grant_type=authorization_code&code=${authorizationCode}&redirect_uri=${redirectUri}`, { headers: { "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Basic ${Buffer.from( - `${this.clientId}:${this.clientSecret}`, - ).toString("base64")}`, + Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")}`, }, - }, + } ); console.log(response.data); diff --git a/src/services/weatherPollingService.ts b/src/services/weatherPollingService.ts index 7c82ebe..2587117 100644 --- a/src/services/weatherPollingService.ts +++ b/src/services/weatherPollingService.ts @@ -1,10 +1,8 @@ -import {appEventBus, USER_UPDATED_EVENT, WEATHER_STATE_UPDATED_EVENT} from "../utils/eventBus"; +import { appEventBus, USER_UPDATED_EVENT, WEATHER_STATE_UPDATED_EVENT } from "../utils/eventBus"; import { getCurrentWeather } from "./owmApiService"; -import {IUser} from "../db/models/user"; - +import { IUser } from "../db/models/user"; export class WeatherPollingService { - private readonly activeLocationPolls: Map; private readonly locationSubscriptions: Map>; private readonly weatherCache: Map; @@ -53,7 +51,7 @@ export class WeatherPollingService { this.weatherCache.delete(location); } } - this.userLocationCache.delete(uuid) + this.userLocationCache.delete(uuid); } private _startPollingForLocation(location: string): void { @@ -101,4 +99,4 @@ export class WeatherPollingService { this.subscribeUser(uuid, newLocation); } } -} \ No newline at end of file +} diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts index 0b51d76..22ee356 100644 --- a/src/types/custom.d.ts +++ b/src/types/custom.d.ts @@ -1,4 +1,4 @@ -import {DecodedToken} from "../interfaces/decodedToken"; +import { DecodedToken } from "../interfaces/decodedToken"; declare global { declare namespace Express { diff --git a/src/utils/eventBus.ts b/src/utils/eventBus.ts index 7a20826..ed15701 100644 --- a/src/utils/eventBus.ts +++ b/src/utils/eventBus.ts @@ -1,6 +1,6 @@ -import { EventEmitter } from 'events'; +import { EventEmitter } from "events"; export const appEventBus = new EventEmitter(); -export const USER_UPDATED_EVENT = 'user:updated'; -export const SPOTIFY_STATE_UPDATED_EVENT = 'spotify:updated'; -export const WEATHER_STATE_UPDATED_EVENT = 'weather:updated'; +export const USER_UPDATED_EVENT = "user:updated"; +export const SPOTIFY_STATE_UPDATED_EVENT = "spotify:updated"; +export const WEATHER_STATE_UPDATED_EVENT = "weather:updated"; diff --git a/src/utils/jwtAuthenticator.ts b/src/utils/jwtAuthenticator.ts index b10aae2..de85832 100644 --- a/src/utils/jwtAuthenticator.ts +++ b/src/utils/jwtAuthenticator.ts @@ -2,23 +2,23 @@ import jwt from "jsonwebtoken"; import { DecodedToken } from "../interfaces/decodedToken"; export class JwtAuthenticator { - constructor(private secret: string) {} + constructor(private secret: string) {} - public verifyToken(token: string | undefined): DecodedToken | null { - if (!token) { - return null; + public verifyToken(token: string | undefined): DecodedToken | null { + if (!token) { + return null; + } + + try { + return jwt.verify(token, this.secret) as DecodedToken; + } catch (error) { + console.error("Error while verifying token:", error); + } + + return null; } - try { - return jwt.verify(token, this.secret) as DecodedToken; - } catch (error) { - console.error("Error while verifying token:", error); + public generateToken(payload: DecodedToken): string { + return jwt.sign(payload, this.secret); } - - return null; - } - - public generateToken(payload: DecodedToken): string { - return jwt.sign(payload, this.secret); - } } diff --git a/src/utils/passwordUtils.ts b/src/utils/passwordUtils.ts index 7c2ca82..5479ced 100644 --- a/src/utils/passwordUtils.ts +++ b/src/utils/passwordUtils.ts @@ -6,9 +6,7 @@ export type ValidationResult = { }; export class PasswordUtils { - - private constructor() { - } + private constructor() {} public static async hashPassword(password: string): Promise { return bcrypt.hash(password, 10); @@ -26,26 +24,25 @@ export class PasswordUtils { const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); if (password.length < minLength) { - return {valid: false, message: `Passwort muss mindestens ${minLength} Zeichen lang sein.`}; + return { valid: false, message: `Passwort muss mindestens ${minLength} Zeichen lang sein.` }; } if (!hasUpperCase) { - return {valid: false, message: "Passwort muss mindestens einen Großbuchstaben enthalten."}; + return { valid: false, message: "Passwort muss mindestens einen Großbuchstaben enthalten." }; } if (!hasLowerCase) { - return {valid: false, message: "Passwort muss mindestens einen Kleinbuchstaben enthalten."}; + return { valid: false, message: "Passwort muss mindestens einen Kleinbuchstaben enthalten." }; } if (!hasNumber) { - return {valid: false, message: "Passwort muss mindestens eine Zahl enthalten."}; + return { valid: false, message: "Passwort muss mindestens eine Zahl enthalten." }; } if (!hasSpecialChar) { - return {valid: false, message: "Passwort muss mindestens ein Sonderzeichen enthalten."}; + return { valid: false, message: "Passwort muss mindestens ein Sonderzeichen enthalten." }; } - return {valid: true, message: "Passwort ist gültig."}; + return { valid: true, message: "Passwort ist gültig." }; } - } diff --git a/src/utils/verifyClient.ts b/src/utils/verifyClient.ts index 805c7f8..2a29692 100644 --- a/src/utils/verifyClient.ts +++ b/src/utils/verifyClient.ts @@ -1,30 +1,24 @@ import "dotenv/config"; -import {IncomingMessage} from "node:http"; -import {ExtendedIncomingMessage} from "../interfaces/extendedIncomingMessage"; -import {JwtAuthenticator} from "./jwtAuthenticator"; +import { IncomingMessage } from "node:http"; +import { ExtendedIncomingMessage } from "../interfaces/extendedIncomingMessage"; +import { JwtAuthenticator } from "./jwtAuthenticator"; export function verifyClient( request: IncomingMessage, jwtAuthenticator: JwtAuthenticator, - callback: (res: boolean, code?: number, message?: string) => void, + callback: (res: boolean, code?: number, message?: string) => void ) { - const token = jwtAuthenticator.verifyToken(request.headers["authorization"]?.slice("Bearer ".length)); - if (!token) { - reject(request, callback); - } else { - (request as ExtendedIncomingMessage).payload = token; - callback(true); - } + const token = jwtAuthenticator.verifyToken(request.headers["authorization"]?.slice("Bearer ".length)); + if (!token) { + reject(request, callback); + } else { + (request as ExtendedIncomingMessage).payload = token; + callback(true); + } } -const reject = ( - request: IncomingMessage, - callback: (res: boolean, code?: number, message?: string) => void, -) => { - console.log( - "Connection refused", - `${request.socket.remoteAddress}:${request.socket.remotePort}`, - ); - callback(false, 401, "Unauthorized"); +const reject = (request: IncomingMessage, callback: (res: boolean, code?: number, message?: string) => void) => { + console.log("Connection refused", `${request.socket.remoteAddress}:${request.socket.remotePort}`); + callback(false, 401, "Unauthorized"); }; diff --git a/src/utils/websocket/websocketCustomEvents/NoData.ts b/src/utils/websocket/websocketCustomEvents/NoData.ts index 8844ba2..5985454 100644 --- a/src/utils/websocket/websocketCustomEvents/NoData.ts +++ b/src/utils/websocket/websocketCustomEvents/NoData.ts @@ -1,3 +1 @@ -export interface NoData{ - -} \ No newline at end of file +export interface NoData {} diff --git a/src/utils/websocket/websocketCustomEvents/customWebsocketEvent.ts b/src/utils/websocket/websocketCustomEvents/customWebsocketEvent.ts index 1c429e6..d00ee89 100644 --- a/src/utils/websocket/websocketCustomEvents/customWebsocketEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/customWebsocketEvent.ts @@ -1,4 +1,4 @@ -import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; +import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket"; export abstract class CustomWebsocketEvent { abstract event: string; @@ -8,5 +8,4 @@ export abstract class CustomWebsocketEvent { public constructor(ws: ExtendedWebSocket) { this.ws = ws; } - } diff --git a/src/utils/websocket/websocketCustomEvents/errorEvent.ts b/src/utils/websocket/websocketCustomEvents/errorEvent.ts index ff74743..d638077 100644 --- a/src/utils/websocket/websocketCustomEvents/errorEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/errorEvent.ts @@ -1,10 +1,10 @@ -import {WebsocketEventType} from "./websocketEventType"; -import {CustomWebsocketEvent} from "./customWebsocketEvent"; -import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; +import { WebsocketEventType } from "./websocketEventType"; +import { CustomWebsocketEvent } from "./customWebsocketEvent"; +import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket"; interface ErrorData { message: string; - traceback: string + traceback: string; } export class ErrorEvent extends CustomWebsocketEvent { @@ -17,5 +17,5 @@ export class ErrorEvent extends CustomWebsocketEvent { handler = async (data: ErrorData) => { console.warn("Error message received", data.message); console.warn("Traceback", data.traceback); - } + }; } diff --git a/src/utils/websocket/websocketCustomEvents/getSettingsEvent.ts b/src/utils/websocket/websocketCustomEvents/getSettingsEvent.ts index 2b41a6e..b3c2372 100644 --- a/src/utils/websocket/websocketCustomEvents/getSettingsEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/getSettingsEvent.ts @@ -1,18 +1,20 @@ -import {CustomWebsocketEvent} from "./customWebsocketEvent"; -import {WebsocketEventType} from "./websocketEventType"; -import {NoData} from "./NoData"; +import { CustomWebsocketEvent } from "./customWebsocketEvent"; +import { WebsocketEventType } from "./websocketEventType"; +import { NoData } from "./NoData"; export class GetSettingsEvent extends CustomWebsocketEvent { - event = WebsocketEventType.GET_SETTINGS; handler = async () => { console.log("Getting settings"); - this.ws.send(JSON.stringify({ - type: "SETTINGS", - payload: { - timezone: this.ws.user.timezone, - }, - }), {binary: false}); - } + this.ws.send( + JSON.stringify({ + type: "SETTINGS", + payload: { + timezone: this.ws.user.timezone, + }, + }), + { binary: false } + ); + }; } diff --git a/src/utils/websocket/websocketCustomEvents/getSpotifyUpdatesEvent.ts b/src/utils/websocket/websocketCustomEvents/getSpotifyUpdatesEvent.ts index 31267a4..0013851 100644 --- a/src/utils/websocket/websocketCustomEvents/getSpotifyUpdatesEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/getSpotifyUpdatesEvent.ts @@ -1,14 +1,16 @@ import { SpotifyPollingService } from "../../../services/spotifyPollingService"; -import {WebsocketEventType} from "./websocketEventType"; -import {NoData} from "./NoData"; -import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; -import {CustomWebsocketEvent} from "./customWebsocketEvent"; - +import { WebsocketEventType } from "./websocketEventType"; +import { NoData } from "./NoData"; +import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket"; +import { CustomWebsocketEvent } from "./customWebsocketEvent"; export class GetSpotifyUpdatesEvent extends CustomWebsocketEvent { event = WebsocketEventType.GET_SPOTIFY_UPDATE; - constructor(ws: ExtendedWebSocket, private spotifyPollingService: SpotifyPollingService) { + constructor( + ws: ExtendedWebSocket, + private spotifyPollingService: SpotifyPollingService + ) { super(ws); } @@ -17,5 +19,5 @@ export class GetSpotifyUpdatesEvent extends CustomWebsocketEvent { if (this.ws.user) { this.spotifyPollingService.startPollingForUser(this.ws.user); } - } + }; } diff --git a/src/utils/websocket/websocketCustomEvents/getStateEvent.ts b/src/utils/websocket/websocketCustomEvents/getStateEvent.ts index 7bd830b..5530644 100644 --- a/src/utils/websocket/websocketCustomEvents/getStateEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/getStateEvent.ts @@ -1,6 +1,6 @@ -import {CustomWebsocketEvent} from "./customWebsocketEvent"; -import {WebsocketEventType} from "./websocketEventType"; -import {NoData} from "./NoData"; +import { CustomWebsocketEvent } from "./customWebsocketEvent"; +import { WebsocketEventType } from "./websocketEventType"; +import { NoData } from "./NoData"; export class GetStateEvent extends CustomWebsocketEvent { event = WebsocketEventType.GET_STATE; @@ -11,6 +11,6 @@ export class GetStateEvent extends CustomWebsocketEvent { type: "STATE", payload: this.ws.user.lastState, }; - this.ws.send(JSON.stringify(messageToSend), {binary: false}); - } + this.ws.send(JSON.stringify(messageToSend), { binary: false }); + }; } diff --git a/src/utils/websocket/websocketCustomEvents/getWeatherUpdatesEvent.ts b/src/utils/websocket/websocketCustomEvents/getWeatherUpdatesEvent.ts index af25609..1b716e0 100644 --- a/src/utils/websocket/websocketCustomEvents/getWeatherUpdatesEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/getWeatherUpdatesEvent.ts @@ -1,15 +1,14 @@ -import {CustomWebsocketEvent} from "./customWebsocketEvent"; -import {WebsocketEventType} from "./websocketEventType"; -import {NoData} from "./NoData"; -import {WeatherPollingService} from "../../../services/weatherPollingService"; -import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; +import { CustomWebsocketEvent } from "./customWebsocketEvent"; +import { WebsocketEventType } from "./websocketEventType"; +import { NoData } from "./NoData"; +import { WeatherPollingService } from "../../../services/weatherPollingService"; +import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket"; export class GetWeatherUpdatesEvent extends CustomWebsocketEvent { - event = WebsocketEventType.GET_WEATHER_UPDATES; private readonly weatherPollingService: WeatherPollingService; - constructor(ws: ExtendedWebSocket, weatherPollingService:WeatherPollingService) { + constructor(ws: ExtendedWebSocket, weatherPollingService: WeatherPollingService) { super(ws); this.weatherPollingService = weatherPollingService; } @@ -19,6 +18,5 @@ export class GetWeatherUpdatesEvent extends CustomWebsocketEvent { if (user?.location && user.uuid) { this.weatherPollingService.subscribeUser(user.uuid, user.location); } - } + }; } - diff --git a/src/utils/websocket/websocketCustomEvents/stopSpotifyUpdatesEvent.ts b/src/utils/websocket/websocketCustomEvents/stopSpotifyUpdatesEvent.ts index d9d1292..42b2028 100644 --- a/src/utils/websocket/websocketCustomEvents/stopSpotifyUpdatesEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/stopSpotifyUpdatesEvent.ts @@ -1,11 +1,10 @@ -import {CustomWebsocketEvent} from "./customWebsocketEvent"; -import {WebsocketEventType} from "./websocketEventType"; -import {NoData} from "./NoData"; -import {SpotifyPollingService} from "../../../services/spotifyPollingService"; -import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; +import { CustomWebsocketEvent } from "./customWebsocketEvent"; +import { WebsocketEventType } from "./websocketEventType"; +import { NoData } from "./NoData"; +import { SpotifyPollingService } from "../../../services/spotifyPollingService"; +import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket"; export class StopSpotifyUpdatesEvent extends CustomWebsocketEvent { - event = WebsocketEventType.STOP_SPOTIFY_UPDATES; private readonly spotifyPollingService: SpotifyPollingService; @@ -25,6 +24,5 @@ export class StopSpotifyUpdatesEvent extends CustomWebsocketEvent { } else { console.warn("Could not stop Spotify polling: No UUID found on WebSocket payload."); } - } - + }; } diff --git a/src/utils/websocket/websocketCustomEvents/stopWeatherUpdatesEvent.ts b/src/utils/websocket/websocketCustomEvents/stopWeatherUpdatesEvent.ts index 002a34d..9a206fc 100644 --- a/src/utils/websocket/websocketCustomEvents/stopWeatherUpdatesEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/stopWeatherUpdatesEvent.ts @@ -1,15 +1,14 @@ -import {CustomWebsocketEvent} from "./customWebsocketEvent"; -import {WebsocketEventType} from "./websocketEventType"; -import {NoData} from "./NoData"; -import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; -import {WeatherPollingService} from "../../../services/weatherPollingService"; +import { CustomWebsocketEvent } from "./customWebsocketEvent"; +import { WebsocketEventType } from "./websocketEventType"; +import { NoData } from "./NoData"; +import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket"; +import { WeatherPollingService } from "../../../services/weatherPollingService"; export class StopWeatherUpdatesEvent extends CustomWebsocketEvent { - event = WebsocketEventType.STOP_WEATHER_UPDATES; private readonly weatherPollingService: WeatherPollingService; - constructor(ws:ExtendedWebSocket, weatherPollingService:WeatherPollingService) { + constructor(ws: ExtendedWebSocket, weatherPollingService: WeatherPollingService) { super(ws); this.weatherPollingService = weatherPollingService; } @@ -20,6 +19,5 @@ export class StopWeatherUpdatesEvent extends CustomWebsocketEvent { if (user?.location && user.uuid) { this.weatherPollingService.unsubscribeUser(user.uuid, user.location); } - } - + }; } diff --git a/src/utils/websocket/websocketCustomEvents/updateUserEvent.ts b/src/utils/websocket/websocketCustomEvents/updateUserEvent.ts index df36947..a48fc96 100644 --- a/src/utils/websocket/websocketCustomEvents/updateUserEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/updateUserEvent.ts @@ -1,16 +1,15 @@ -import {WebsocketEventType} from "./websocketEventType"; -import {CustomWebsocketEvent} from "./customWebsocketEvent"; -import {IUser} from "../../../db/models/user"; +import { WebsocketEventType } from "./websocketEventType"; +import { CustomWebsocketEvent } from "./customWebsocketEvent"; +import { IUser } from "../../../db/models/user"; export class UpdateUserSingleEvent extends CustomWebsocketEvent { event = WebsocketEventType.UPDATE_USER_SINGLE; handler = async (data: IUser) => { - console.log("Updating user") + console.log("Updating user"); if (data) { this.ws.user = data; - console.log("User updated") + console.log("User updated"); } - } + }; } - diff --git a/src/utils/websocket/websocketCustomEvents/websocketEventUtils.ts b/src/utils/websocket/websocketCustomEvents/websocketEventUtils.ts index 2a54b43..36dcb3d 100644 --- a/src/utils/websocket/websocketCustomEvents/websocketEventUtils.ts +++ b/src/utils/websocket/websocketCustomEvents/websocketEventUtils.ts @@ -1,17 +1,21 @@ -import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; -import {GetSettingsEvent} from "./getSettingsEvent"; -import {ErrorEvent} from "./errorEvent"; -import {GetSpotifyUpdatesEvent} from "./getSpotifyUpdatesEvent"; -import {GetStateEvent} from "./getStateEvent"; -import {GetWeatherUpdatesEvent} from "./getWeatherUpdatesEvent"; -import {StopSpotifyUpdatesEvent} from "./stopSpotifyUpdatesEvent"; -import {StopWeatherUpdatesEvent} from "./stopWeatherUpdatesEvent"; -import { UpdateUserSingleEvent} from "./updateUserEvent"; -import {CustomWebsocketEvent} from "./customWebsocketEvent"; -import {SpotifyPollingService} from "../../../services/spotifyPollingService"; -import {WeatherPollingService} from "../../../services/weatherPollingService"; +import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket"; +import { GetSettingsEvent } from "./getSettingsEvent"; +import { ErrorEvent } from "./errorEvent"; +import { GetSpotifyUpdatesEvent } from "./getSpotifyUpdatesEvent"; +import { GetStateEvent } from "./getStateEvent"; +import { GetWeatherUpdatesEvent } from "./getWeatherUpdatesEvent"; +import { StopSpotifyUpdatesEvent } from "./stopSpotifyUpdatesEvent"; +import { StopWeatherUpdatesEvent } from "./stopWeatherUpdatesEvent"; +import { UpdateUserSingleEvent } from "./updateUserEvent"; +import { CustomWebsocketEvent } from "./customWebsocketEvent"; +import { SpotifyPollingService } from "../../../services/spotifyPollingService"; +import { WeatherPollingService } from "../../../services/weatherPollingService"; -export function getEventListeners(ws: ExtendedWebSocket, spotifyPollingService: SpotifyPollingService, weatherPollingService:WeatherPollingService): CustomWebsocketEvent[] { +export function getEventListeners( + ws: ExtendedWebSocket, + spotifyPollingService: SpotifyPollingService, + weatherPollingService: WeatherPollingService +): CustomWebsocketEvent[] { return [ new GetStateEvent(ws), new GetSettingsEvent(ws), @@ -20,6 +24,6 @@ export function getEventListeners(ws: ExtendedWebSocket, spotifyPollingService: new GetWeatherUpdatesEvent(ws, weatherPollingService), new StopWeatherUpdatesEvent(ws, weatherPollingService), new UpdateUserSingleEvent(ws), - new ErrorEvent(ws) + new ErrorEvent(ws), ]; } diff --git a/src/utils/websocket/websocketEventHandler.ts b/src/utils/websocket/websocketEventHandler.ts index dc3f040..ebffb87 100644 --- a/src/utils/websocket/websocketEventHandler.ts +++ b/src/utils/websocket/websocketEventHandler.ts @@ -1,12 +1,15 @@ -import {ExtendedWebSocket} from "../../interfaces/extendedWebsocket"; -import {CustomWebsocketEvent} from "./websocketCustomEvents/customWebsocketEvent"; -import {getEventListeners} from "./websocketCustomEvents/websocketEventUtils"; -import {SpotifyPollingService} from "../../services/spotifyPollingService"; -import {WeatherPollingService} from "../../services/weatherPollingService"; +import { ExtendedWebSocket } from "../../interfaces/extendedWebsocket"; +import { CustomWebsocketEvent } from "./websocketCustomEvents/customWebsocketEvent"; +import { getEventListeners } from "./websocketCustomEvents/websocketEventUtils"; +import { SpotifyPollingService } from "../../services/spotifyPollingService"; +import { WeatherPollingService } from "../../services/weatherPollingService"; export class WebsocketEventHandler { - constructor(private webSocket: ExtendedWebSocket, private spotifyPollingService: SpotifyPollingService, private readonly weatherPollingService: WeatherPollingService) { - } + constructor( + private webSocket: ExtendedWebSocket, + private spotifyPollingService: SpotifyPollingService, + private readonly weatherPollingService: WeatherPollingService + ) {} public enableErrorEvent() { this.webSocket.on("error", console.error); @@ -31,15 +34,14 @@ export class WebsocketEventHandler { public enableMessageEvent() { this.webSocket.on("message", (data) => { - const message = data.toString(); - const messageJson = JSON.parse(message); - const {type} = messageJson; - console.log("Received message:", message); + const message = data.toString(); + const messageJson = JSON.parse(message); + const { type } = messageJson; + console.log("Received message:", message); - // emit event to the custom event handler - this.webSocket.emit(type, messageJson); - } - ); + // emit event to the custom event handler + this.webSocket.emit(type, messageJson); + }); } public registerCustomEvents() { @@ -50,5 +52,4 @@ export class WebsocketEventHandler { private registerCustomEvent(customWebsocketEvent: CustomWebsocketEvent) { this.webSocket.on(customWebsocketEvent.event, customWebsocketEvent.handler.bind(customWebsocketEvent)); } - } diff --git a/src/utils/websocket/websocketServerEventHandler.ts b/src/utils/websocket/websocketServerEventHandler.ts index fe56cdc..31178cf 100644 --- a/src/utils/websocket/websocketServerEventHandler.ts +++ b/src/utils/websocket/websocketServerEventHandler.ts @@ -1,39 +1,39 @@ -import {ExtendedWebSocket} from "../../interfaces/extendedWebsocket"; -import {ExtendedIncomingMessage} from "../../interfaces/extendedIncomingMessage"; -import {Server as WebSocketServer} from "ws"; -import {heartbeat} from "./websocketServerHeartbeatInterval"; -import {UserService} from "../../services/db/UserService"; +import { ExtendedWebSocket } from "../../interfaces/extendedWebsocket"; +import { ExtendedIncomingMessage } from "../../interfaces/extendedIncomingMessage"; +import { Server as WebSocketServer } from "ws"; +import { heartbeat } from "./websocketServerHeartbeatInterval"; +import { UserService } from "../../services/db/UserService"; export class WebsocketServerEventHandler { private readonly heartbeat: () => void; private readonly userService: UserService; - constructor(private webSocketServer: WebSocketServer, userService: UserService) { + constructor( + private webSocketServer: WebSocketServer, + userService: UserService + ) { this.heartbeat = heartbeat(this.webSocketServer); this.userService = userService; } - public enableConnectionEvent( - callback: (ws: ExtendedWebSocket, request: ExtendedIncomingMessage) => void, - ) { - this.webSocketServer.on( - "connection", - async (ws: ExtendedWebSocket, request: ExtendedIncomingMessage) => { - const user = await this.userService.getUserByUUID(request.payload.uuid); + public enableConnectionEvent(callback: (ws: ExtendedWebSocket, request: ExtendedIncomingMessage) => void) { + this.webSocketServer.on("connection", async (ws: ExtendedWebSocket, request: ExtendedIncomingMessage) => { + const user = await this.userService.getUserByUUID(request.payload.uuid); - if (!user) { ws.terminate(); return; } - ws.user = user; + if (!user) { + ws.terminate(); + return; + } + ws.user = user; - // first: map the payload from the request to the ws object (is payloed needed anymore?) - ws.payload = request.payload; - // second: set the isAlive flag to true - ws.isAlive = true; + // first: map the payload from the request to the ws object (is payloed needed anymore?) + ws.payload = request.payload; + // second: set the isAlive flag to true + ws.isAlive = true; - - // last: call the callback function - callback(ws, request); - }, - ); + // last: call the callback function + callback(ws, request); + }); } public enableHeartbeat(interval: number) { diff --git a/src/utils/websocket/websocketServerHeartbeatInterval.ts b/src/utils/websocket/websocketServerHeartbeatInterval.ts index 5a9a20f..fc84011 100644 --- a/src/utils/websocket/websocketServerHeartbeatInterval.ts +++ b/src/utils/websocket/websocketServerHeartbeatInterval.ts @@ -2,21 +2,13 @@ import { WebSocket, WebSocketServer } from "ws"; import { DecodedToken } from "../../interfaces/decodedToken"; export function heartbeat(wss: WebSocketServer) { - return () => { - wss.clients.forEach( - (ws: WebSocket & { isAlive?: boolean; payload?: DecodedToken }) => { - console.log( - new Date().toLocaleString("de-DE") + - ":" + - ws.payload?.username + - ": isAlive: " + - ws.isAlive, - ); - if (!ws.isAlive) return ws.terminate(); + return () => { + wss.clients.forEach((ws: WebSocket & { isAlive?: boolean; payload?: DecodedToken }) => { + console.log(new Date().toLocaleString("de-DE") + ":" + ws.payload?.username + ": isAlive: " + ws.isAlive); + if (!ws.isAlive) return ws.terminate(); - ws.isAlive = false; - ws.ping(); - }, - ); - }; + ws.isAlive = false; + ws.ping(); + }); + }; } diff --git a/src/websocket.ts b/src/websocket.ts index 41b8c97..683fd86 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,21 +1,21 @@ -import {Server} from "http"; -import {Server as WebSocketServer, WebSocket} from "ws"; -import {verifyClient} from "./utils/verifyClient"; -import {ExtendedWebSocket} from "./interfaces/extendedWebsocket"; -import {WebsocketServerEventHandler} from "./utils/websocket/websocketServerEventHandler"; -import {WebsocketEventHandler} from "./utils/websocket/websocketEventHandler"; -import {WebsocketEventType} from "./utils/websocket/websocketCustomEvents/websocketEventType"; +import { Server } from "http"; +import { Server as WebSocketServer, WebSocket } from "ws"; +import { verifyClient } from "./utils/verifyClient"; +import { ExtendedWebSocket } from "./interfaces/extendedWebsocket"; +import { WebsocketServerEventHandler } from "./utils/websocket/websocketServerEventHandler"; +import { WebsocketEventHandler } from "./utils/websocket/websocketEventHandler"; +import { WebsocketEventType } from "./utils/websocket/websocketCustomEvents/websocketEventType"; import { appEventBus, SPOTIFY_STATE_UPDATED_EVENT, USER_UPDATED_EVENT, - WEATHER_STATE_UPDATED_EVENT + WEATHER_STATE_UPDATED_EVENT, } from "./utils/eventBus"; -import {IUser} from "./db/models/user"; -import {SpotifyPollingService} from "./services/spotifyPollingService"; -import {UserService} from "./services/db/UserService"; -import {WeatherPollingService} from "./services/weatherPollingService"; -import {JwtAuthenticator} from "./utils/jwtAuthenticator"; +import { IUser } from "./db/models/user"; +import { SpotifyPollingService } from "./services/spotifyPollingService"; +import { UserService } from "./services/db/UserService"; +import { WeatherPollingService } from "./services/weatherPollingService"; +import { JwtAuthenticator } from "./utils/jwtAuthenticator"; export class ExtendedWebSocketServer { private readonly _wss: WebSocketServer; @@ -23,8 +23,13 @@ export class ExtendedWebSocketServer { private readonly spotifyPollingService: SpotifyPollingService; private readonly weatherPollingService: WeatherPollingService; - constructor(server: Server, userService: UserService, spotifyPollingService: SpotifyPollingService, - weatherPollingService: WeatherPollingService, jwtAuthenticator: JwtAuthenticator) { + constructor( + server: Server, + userService: UserService, + spotifyPollingService: SpotifyPollingService, + weatherPollingService: WeatherPollingService, + jwtAuthenticator: JwtAuthenticator + ) { this.userService = userService; this.spotifyPollingService = spotifyPollingService; this.weatherPollingService = weatherPollingService; @@ -41,7 +46,7 @@ export class ExtendedWebSocketServer { public broadcast(message: string): void { this.getConnectedClients().forEach((client) => { if (client.readyState === WebSocket.OPEN) { - client.send(message, {binary: false}); + client.send(message, { binary: false }); } }); } @@ -49,7 +54,7 @@ export class ExtendedWebSocketServer { public sendMessageToUser(uuid: string, message: string): void { const client = this._findClientByUUID(uuid); if (client && client.readyState === WebSocket.OPEN) { - client.send(message, {binary: false}); + client.send(message, { binary: false }); } } @@ -73,7 +78,11 @@ export class ExtendedWebSocketServer { private _onNewClientReady(ws: ExtendedWebSocket): void { console.log("WebSocket client connected and authenticated"); - const socketEventHandler = new WebsocketEventHandler(ws, this.spotifyPollingService, this.weatherPollingService); + const socketEventHandler = new WebsocketEventHandler( + ws, + this.spotifyPollingService, + this.weatherPollingService + ); socketEventHandler.enableErrorEvent(); socketEventHandler.enablePongEvent(); @@ -98,29 +107,34 @@ export class ExtendedWebSocketServer { } }); - appEventBus.on(SPOTIFY_STATE_UPDATED_EVENT, ({uuid, state}) => { + appEventBus.on(SPOTIFY_STATE_UPDATED_EVENT, ({ uuid, state }) => { const client = this._findClientByUUID(uuid); console.log(`Received update for user ${uuid}`); if (client) { - client.send(JSON.stringify({ - type: "SPOTIFY_UPDATE", - payload: state, - }), {binary: false}); + client.send( + JSON.stringify({ + type: "SPOTIFY_UPDATE", + payload: state, + }), + { binary: false } + ); } }); - appEventBus.on(WEATHER_STATE_UPDATED_EVENT, ({weatherData, subscribers}) => { + appEventBus.on(WEATHER_STATE_UPDATED_EVENT, ({ weatherData, subscribers }) => { for (const uuid of subscribers) { const client = this._findClientByUUID(uuid); if (client) { - client.send(JSON.stringify({ - type: "WEATHER_UPDATE", - payload: weatherData, - }), {binary: false}); + client.send( + JSON.stringify({ + type: "WEATHER_UPDATE", + payload: weatherData, + }), + { binary: false } + ); } } }); - } private _findClientByUUID(uuid: string): ExtendedWebSocket | undefined { @@ -131,4 +145,4 @@ export class ExtendedWebSocketServer { } return undefined; } -} \ No newline at end of file +}