run prettier

This commit is contained in:
StarAppeal
2025-09-25 02:40:25 +02:00
parent e935ba66f3
commit 6872f57bca
50 changed files with 750 additions and 721 deletions
+2 -2
View File
@@ -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 PORT = optionalNumber("PORT", process.env.PORT, 3000);
const FRONTEND_URL = required("FRONTEND_URL", process.env.FRONTEND_URL); 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"); throw new Error("FRONTEND_URL must be a valid URL");
} }
export const config :BaseConfig = { export const config: BaseConfig = {
env: NODE_ENV, env: NODE_ENV,
port: PORT, port: PORT,
cors: { cors: {
+81 -69
View File
@@ -1,6 +1,6 @@
import "dotenv/config"; import "dotenv/config";
import mongoose, {Schema, Document} from "mongoose"; import mongoose, { Schema, Document } from "mongoose";
import {PasswordUtils} from "../../utils/passwordUtils"; import { PasswordUtils } from "../../utils/passwordUtils";
export interface IUser extends Document { export interface IUser extends Document {
name: string; name: string;
@@ -14,10 +14,10 @@ export interface IUser extends Document {
} }
export interface CreateUserPayload { export interface CreateUserPayload {
name: string, name: string;
password: string, password: string;
uuid: string, uuid: string;
config: UserConfig, config: UserConfig;
timezone: string; timezone: string;
location: string; location: string;
} }
@@ -30,12 +30,12 @@ export interface UserConfig {
export interface MatrixState { export interface MatrixState {
global: { global: {
mode: 'image' | 'text' | "idle" | "music" | "clock"; mode: "image" | "text" | "idle" | "music" | "clock";
brightness: number; brightness: number;
}; };
text: { text: {
text: string; text: string;
align: 'left' | 'center' | 'right'; align: "left" | "center" | "right";
speed: number; speed: number;
size: number; size: number;
color: [number, number, number]; color: [number, number, number];
@@ -58,71 +58,83 @@ export interface SpotifyConfig {
scope: string; scope: string;
} }
const matrixStateSchema = new Schema({ const matrixStateSchema = new Schema(
global: { {
mode: {type: String, enum: ['image', 'text', 'idle', 'music', 'clock'], default: 'idle'}, global: {
brightness: {type: Number, min: 0, max: 100, default: 50}, 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: ""}, text: {
align: {type: String, enum: ['left', 'center', 'right'], default: 'center'}, text: { type: String, default: "" },
speed: {type: Number, min: 0, max: 10, default: 3}, align: { type: String, enum: ["left", "center", "right"], default: "center" },
size: {type: Number, min: 1, max: 64, default: 12}, speed: { type: Number, min: 0, max: 10, default: 3 },
color: { size: { type: Number, min: 1, max: 64, default: 12 },
type: [Number], color: {
validate: { type: [Number],
validator: (v: number[]) => validate: {
Array.isArray(v) && v.length === 3 && v.every(n => Number.isInteger(n) && n >= 0 && n <= 255), validator: (v: number[]) =>
message: "color must be an array of three integers between 0 and 255", 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: { { _id: false }
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});
const spotifyConfigSchema = new Schema({ const spotifyConfigSchema = new Schema(
accessToken: {type: String}, {
refreshToken: {type: String}, accessToken: { type: String },
expirationDate: {type: Date}, refreshToken: { type: String },
scope: {type: String}, expirationDate: { type: Date },
}, {_id: false}); scope: { type: String },
},
{ _id: false }
);
const userConfigSchema = new Schema({ const userConfigSchema = new Schema(
isVisible: {type: Boolean, required: true}, {
canBeModified: {type: Boolean, required: true}, isVisible: { type: Boolean, required: true },
isAdmin: {type: Boolean, required: true}, canBeModified: { type: Boolean, required: true },
}, {_id: false}); isAdmin: { type: Boolean, required: true },
},
{ _id: false }
);
const userSchema = new Schema({ const userSchema = new Schema(
name: {type: String, required: true, index: true}, {
password: {type: String, required: true, select: false}, name: { type: String, required: true, index: true },
uuid: {type: String, required: true, unique: true, index: true}, password: { type: String, required: true, select: false },
config: {type: userConfigSchema, required: true}, uuid: { type: String, required: true, unique: true, index: true },
lastState: {type: matrixStateSchema}, config: { type: userConfigSchema, required: true },
spotifyConfig: {type: spotifyConfigSchema}, lastState: { type: matrixStateSchema },
timezone: {type: String, required: true}, spotifyConfig: { type: spotifyConfigSchema },
location: {type: String, required: true}, timezone: { type: String, required: true },
}, { location: { type: String, required: true },
optimisticConcurrency: true, },
timestamps: true, {
}); optimisticConcurrency: true,
timestamps: true,
}
);
userSchema.virtual("id").get(function (this: any) { userSchema.virtual("id").get(function (this: any) {
return this._id?.toHexString?.() ?? this._id; return this._id?.toHexString?.() ?? this._id;
@@ -136,7 +148,7 @@ async function hashIfNeeded(next: Function, user: any) {
if (!user.isModified?.("password")) return next(); if (!user.isModified?.("password")) return next();
if (isBcryptHash(user.password)) return next(); if (isBcryptHash(user.password)) return next();
try { try {
user.password = await PasswordUtils.hashPassword(user.password) user.password = await PasswordUtils.hashPassword(user.password);
return next(); return next();
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -165,4 +177,4 @@ userSchema.pre("findOneAndUpdate", async function (next) {
} }
}); });
export const UserModel = mongoose.model<IUser>('User', userSchema); export const UserModel = mongoose.model<IUser>("User", userSchema);
+5 -5
View File
@@ -1,11 +1,11 @@
import { UserModel } from './user'; import { UserModel } from "./user";
import {appEventBus, USER_UPDATED_EVENT} from "../../utils/eventBus"; import { appEventBus, USER_UPDATED_EVENT } from "../../utils/eventBus";
export function watchUserChanges() { export function watchUserChanges() {
const changeStream = UserModel.watch([], { fullDocument: 'updateLookup' }); const changeStream = UserModel.watch([], { fullDocument: "updateLookup" });
changeStream.on('change', (change: any) => { changeStream.on("change", (change: any) => {
if (change.operationType === 'update' && change.fullDocument) { if (change.operationType === "update" && change.fullDocument) {
const updatedUser = change.fullDocument; const updatedUser = change.fullDocument;
appEventBus.emit(USER_UPDATED_EVENT, updatedUser); appEventBus.emit(USER_UPDATED_EVENT, updatedUser);
+33 -33
View File
@@ -1,13 +1,13 @@
import {Server} from "./server"; import { Server } from "./server";
import {config as baseConfig} from "./config/config"; import { config as baseConfig } from "./config/config";
import {S3Service} from "./services/s3Service"; import { S3Service } from "./services/s3Service";
import {UserService} from "./services/db/UserService"; import { UserService } from "./services/db/UserService";
import {SpotifyTokenService} from "./services/spotifyTokenService"; import { SpotifyTokenService } from "./services/spotifyTokenService";
import {connectToDatabase} from "./services/db/database.service"; import { connectToDatabase } from "./services/db/database.service";
import {SpotifyApiService} from "./services/spotifyApiService"; import { SpotifyApiService } from "./services/spotifyApiService";
import {SpotifyPollingService} from "./services/spotifyPollingService"; import { SpotifyPollingService } from "./services/spotifyPollingService";
import {WeatherPollingService} from "./services/weatherPollingService"; import { WeatherPollingService } from "./services/weatherPollingService";
import {JwtAuthenticator} from "./utils/jwtAuthenticator"; import { JwtAuthenticator } from "./utils/jwtAuthenticator";
async function bootstrap() { async function bootstrap() {
const { const {
@@ -20,7 +20,7 @@ async function bootstrap() {
MINIO_ROOT_USER, MINIO_ROOT_USER,
MINIO_ROOT_PASSWORD, MINIO_ROOT_PASSWORD,
DB_NAME, DB_NAME,
DB_CONN_STRING DB_CONN_STRING,
} = process.env; } = process.env;
if (!SECRET_KEY || SECRET_KEY.length < 32) { if (!SECRET_KEY || SECRET_KEY.length < 32) {
@@ -54,22 +54,19 @@ async function bootstrap() {
port: parseInt(MINIO_PORT), port: parseInt(MINIO_PORT),
accessKey: MINIO_ROOT_USER, accessKey: MINIO_ROOT_USER,
secretAccessKey: MINIO_ROOT_PASSWORD, secretAccessKey: MINIO_ROOT_PASSWORD,
bucket: MINIO_BUCKET_NAME bucket: MINIO_BUCKET_NAME,
}; };
const dbConfig = { const dbConfig = {
dbName: DB_NAME, dbName: DB_NAME,
dbConnString: DB_CONN_STRING dbConnString: DB_CONN_STRING,
} };
await connectToDatabase(dbConfig.dbName, dbConfig.dbConnString); await connectToDatabase(dbConfig.dbName, dbConfig.dbConnString);
const s3Service = S3Service.getInstance(s3ClientConfig); const s3Service = S3Service.getInstance(s3ClientConfig);
const userService = await UserService.create(); const userService = await UserService.create();
const spotifyTokenService = new SpotifyTokenService( const spotifyTokenService = new SpotifyTokenService(SPOTIFY_CLIENT_ID!, SPOTIFY_CLIENT_SECRET!);
SPOTIFY_CLIENT_ID!,
SPOTIFY_CLIENT_SECRET!
);
const spotifyApiService = new SpotifyApiService(); const spotifyApiService = new SpotifyApiService();
const spotifyPollingService = new SpotifyPollingService(userService, spotifyApiService, spotifyTokenService); const spotifyPollingService = new SpotifyPollingService(userService, spotifyApiService, spotifyTokenService);
@@ -77,25 +74,28 @@ async function bootstrap() {
const jwtAuthenticator = new JwtAuthenticator(SECRET_KEY); const jwtAuthenticator = new JwtAuthenticator(SECRET_KEY);
const server = new Server({ const server = new Server(
port: baseConfig.port, {
jwtSecret: SECRET_KEY, port: baseConfig.port,
cors: baseConfig.cors, jwtSecret: SECRET_KEY,
}, { cors: baseConfig.cors,
s3Service, },
userService, {
spotifyTokenService, s3Service,
spotifyPollingService, userService,
weatherPollingService, spotifyTokenService,
jwtAuthenticator spotifyPollingService,
}); weatherPollingService,
jwtAuthenticator,
}
);
await server.start(); await server.start();
} }
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== "test") {
bootstrap().catch(error => { bootstrap().catch((error) => {
console.error("Fatal error during server startup:", error.message); console.error("Fatal error during server startup:", error.message);
process.exit(1); process.exit(1);
}); });
} }
+1 -1
View File
@@ -18,4 +18,4 @@ export interface CurrentlyPlaying {
duration_ms: number; duration_ms: number;
}; };
is_playing: boolean; is_playing: boolean;
} }
+3 -3
View File
@@ -1,5 +1,5 @@
export interface DecodedToken { export interface DecodedToken {
username: string; username: string;
id: string; id: string;
uuid: string; uuid: string;
} }
+1 -1
View File
@@ -2,5 +2,5 @@ import { IncomingMessage } from "node:http";
import { DecodedToken } from "./decodedToken"; import { DecodedToken } from "./decodedToken";
export interface ExtendedIncomingMessage extends IncomingMessage { export interface ExtendedIncomingMessage extends IncomingMessage {
payload: DecodedToken; payload: DecodedToken;
} }
+3 -3
View File
@@ -1,6 +1,6 @@
import {WebSocket} from "ws"; import { WebSocket } from "ws";
import {DecodedToken} from "./decodedToken"; import { DecodedToken } from "./decodedToken";
import {IUser} from "../db/models/user"; import { IUser } from "../db/models/user";
export interface ExtendedWebSocket extends WebSocket { export interface ExtendedWebSocket extends WebSocket {
payload: DecodedToken; payload: DecodedToken;
+40 -37
View File
@@ -1,12 +1,12 @@
import express from "express"; import express from "express";
import {CreateUserPayload} from "../db/models/user"; import { CreateUserPayload } from "../db/models/user";
import {JwtAuthenticator} from "../utils/jwtAuthenticator"; import { JwtAuthenticator } from "../utils/jwtAuthenticator";
import crypto from "crypto"; import crypto from "crypto";
import {PasswordUtils} from "../utils/passwordUtils"; import { PasswordUtils } from "../utils/passwordUtils";
import {asyncHandler} from "./middleware/asyncHandler"; import { asyncHandler } from "./middleware/asyncHandler";
import {validateBody, v} from "./middleware/validate"; import { validateBody, v } from "./middleware/validate";
import {ok, badRequest, unauthorized, created, conflict, notFound} from "./utils/responses"; import { ok, badRequest, unauthorized, created, conflict, notFound } from "./utils/responses";
import {UserService} from "../services/db/UserService"; import { UserService } from "../services/db/UserService";
export class RestAuth { export class RestAuth {
private readonly userService: UserService; private readonly userService: UserService;
@@ -23,25 +23,28 @@ export class RestAuth {
router.post( router.post(
"/register", "/register",
validateBody({ validateBody({
username: {required: true, validator: v.isString({nonEmpty: true, min: 3})}, username: { required: true, validator: v.isString({ nonEmpty: true, min: 3 }) },
password: {required: true, validator: v.isString({nonEmpty: true, min: 8})}, password: { required: true, validator: v.isString({ nonEmpty: true, min: 8 }) },
timezone: {required: true, validator: v.isString({nonEmpty: true})}, timezone: { required: true, validator: v.isString({ nonEmpty: true }) },
location: {required: true, validator: v.isString({nonEmpty: true})}, location: { required: true, validator: v.isString({ nonEmpty: true }) },
}), }),
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const {username, password, timezone, location} = req.body as { const { username, password, timezone, location } = req.body as {
username: string; password: string; timezone: string; location: string; username: string;
password: string;
timezone: string;
location: string;
}; };
if (await this.userService.existsUserByName(username)) { 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); const passwordValidation = PasswordUtils.validatePassword(password);
if (!passwordValidation.valid) { if (!passwordValidation.valid) {
return badRequest(res, passwordValidation.message ?? "Invalid password", { return badRequest(res, passwordValidation.message ?? "Invalid password", {
field: "password", field: "password",
code: "INVALID_PASSWORD_FORMAT" code: "INVALID_PASSWORD_FORMAT",
}); });
} }
@@ -53,61 +56,61 @@ export class RestAuth {
config: { config: {
isVisible: false, isVisible: false,
isAdmin: false, isAdmin: false,
canBeModified: false canBeModified: false,
}, },
timezone, timezone,
location location,
}; };
const result = await this.userService.createUser(newUser); const result = await this.userService.createUser(newUser);
return created(res, {user: result}); return created(res, { user: result });
}) })
); );
router.post( router.post(
"/login", "/login",
validateBody({ validateBody({
username: {required: true, validator: v.isString({nonEmpty: true})}, username: { required: true, validator: v.isString({ nonEmpty: true }) },
password: {required: true, validator: v.isString({nonEmpty: true})}, password: { required: true, validator: v.isString({ nonEmpty: true }) },
}), }),
asyncHandler(async (req, res) => { 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); const user = await this.userService.getUserAuthByName(username);
if (!user) { 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!); const isValid = await PasswordUtils.comparePassword(password, user.password!);
if (!isValid) { 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({ const jwtToken = this.jwtAuthenticator.generateToken({
username: user.name, username: user.name,
id: user.id, id: user.id,
uuid: user.uuid uuid: user.uuid,
});
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}); 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( router.post(
"/logout", "/logout",
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
res.clearCookie('auth-token'); res.clearCookie("auth-token");
return ok(res, {message: "Successfully logged out"}); return ok(res, { message: "Successfully logged out" });
}) })
); );
return router; return router;
} }
} }
+19 -11
View File
@@ -7,19 +7,27 @@ export class JwtTokenPropertiesExtractor {
public createRouter() { public createRouter() {
const router = express.Router(); const router = express.Router();
router.get("/id", asyncHandler(async (req: Request, res: Response) => { router.get(
return ok(res, req.payload.id); "/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) => { router.get(
return ok(res, req.payload.username); "/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; return router;
} }
} }
+1 -3
View File
@@ -1,8 +1,6 @@
import type { Request, Response, NextFunction, RequestHandler } from "express"; import type { Request, Response, NextFunction, RequestHandler } from "express";
export function asyncHandler( export function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<any>): RequestHandler {
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
): RequestHandler {
return async (req, res, next) => { return async (req, res, next) => {
try { try {
await fn(req, res, next); await fn(req, res, next);
+1 -4
View File
@@ -9,10 +9,7 @@ export function authenticateJwt(jwtAuthenticator: JwtAuthenticator) {
const authHeader = req.headers["authorization"]; const authHeader = req.headers["authorization"];
if (!authHeader) { if (!authHeader) {
return unauthorized( return unauthorized(res, "Unauthorized: No Authorization header provided");
res,
"Unauthorized: No Authorization header provided"
);
} }
if (!authHeader.startsWith(BEARER_PREFIX)) { if (!authHeader.startsWith(BEARER_PREFIX)) {
@@ -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) => { export const extractTokenFromCookie = (req: Request, res: Response, next: NextFunction) => {
if (req.headers.authorization) { if (req.headers.authorization) {
return next(); return next();
} }
const token = req.cookies['auth-token']; const token = req.cookies["auth-token"];
if (token) { if (token) {
req.headers.authorization = `Bearer ${token}`; req.headers.authorization = `Bearer ${token}`;
} }
next(); next();
}; };
+2 -2
View File
@@ -1,6 +1,6 @@
import type { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from "express";
import { notFound } from "../utils/responses"; import { notFound } from "../utils/responses";
import {UserService} from "../../services/db/UserService"; import { UserService } from "../../services/db/UserService";
export function isAdmin(userService: UserService) { export function isAdmin(userService: UserService) {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
@@ -17,4 +17,4 @@ export function isAdmin(userService: UserService) {
return next(error); return next(error);
} }
}; };
} }
+1 -1
View File
@@ -20,4 +20,4 @@ export const spotifyLimiter = rateLimit({
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
handler: onLimitReached, handler: onLimitReached,
}); });
+73 -72
View File
@@ -1,5 +1,5 @@
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from "express";
import {badRequest} from "../utils/responses"; import { badRequest } from "../utils/responses";
/** /**
* A type definition for a validation function. * 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. * Validates whether a value is a valid URL. The value must be a string and conform to standard URL formatting rules.
*/ */
export const v = { export const v = {
isString: (opts?: { nonEmpty?: boolean; max?: number; min?: number }): Validator => { isString: (opts?: { nonEmpty?: boolean; max?: number; min?: number }): Validator => {
return (value: any) => { return (value: any) => {
if (typeof value !== "string") return "must be a string"; 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?.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?.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`; if (opts?.min !== undefined && value.length < opts.min) return `must be at least ${opts.min} chars`;
return true; return true;
}; };
}, },
isNumber: (opts?: { min?: number; max?: number; integer?: boolean }): Validator => { isNumber: (opts?: { min?: number; max?: number; integer?: boolean }): Validator => {
return (value: any) => { return (value: any) => {
if (typeof value !== "number" || Number.isNaN(value)) return "must be a number"; 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?.integer && !Number.isInteger(value)) return "must be an integer";
if (opts?.min !== undefined && value < opts.min) return `must be >= ${opts.min}`; if (opts?.min !== undefined && value < opts.min) return `must be >= ${opts.min}`;
if (opts?.max !== undefined && value > opts.max) return `must be <= ${opts.max}`; if (opts?.max !== undefined && value > opts.max) return `must be <= ${opts.max}`;
return true; return true;
}; };
}, },
isBoolean: (): Validator => { isBoolean: (): Validator => {
return (value: any) => (typeof value === "boolean" ? true : "must be a boolean"); return (value: any) => (typeof value === "boolean" ? true : "must be a boolean");
}, },
isEnum: <T extends readonly string[]>(values: T): Validator => { isEnum: <T extends readonly string[]>(values: T): Validator => {
return (value: any) => (values.includes(value) ? true : `must be one of: ${values.join(", ")}`); return (value: any) => (values.includes(value) ? true : `must be one of: ${values.join(", ")}`);
}, },
isArrayLength: (len: number): Validator => { isArrayLength: (len: number): Validator => {
return (value: any) => (Array.isArray(value) && value.length === len ? true : `must be an array of length ${len}`); 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) => { isObject: (opts?: { nonEmpty?: boolean }): Validator => {
if (typeof value !== "object" || value === null) return "must be an object"; return (value: any) => {
if (opts?.nonEmpty && Object.keys(value).length === 0) return "must be a non-empty object"; if (typeof value !== "object" || value === null) return "must be an object";
return true; if (opts?.nonEmpty && Object.keys(value).length === 0) return "must be a non-empty object";
}; return true;
}, };
isUrl: (): Validator => { },
return (value: any) => { isUrl: (): Validator => {
if (typeof value !== "string") return "must be a string URL"; return (value: any) => {
try { if (typeof value !== "string") return "must be a string URL";
new URL(value); try {
return true; new URL(value);
} catch { return true;
return "must be a valid URL"; } catch {
} return "must be a valid URL";
}; }
}, };
},
}; };
/** /**
@@ -106,17 +107,17 @@ type Schema = Record<string, { required?: boolean; validator: Validator }>;
* @return {string[]} An array of error messages. If there are no validation errors, the array will be empty. * @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[] { function validate(source: any, schema: Schema): string[] {
const errors: string[] = []; const errors: string[] = [];
for (const [key, rule] of Object.entries(schema)) { for (const [key, rule] of Object.entries(schema)) {
const value = source?.[key]; const value = source?.[key];
if (value === undefined || value === null) { if (value === undefined || value === null) {
if (rule.required) errors.push(`${key} is required`); if (rule.required) errors.push(`${key} is required`);
continue; continue;
}
const res = rule.validator(value);
if (res !== true) errors.push(`${key} ${res}`);
} }
const res = rule.validator(value); return errors;
if (res !== true) errors.push(`${key} ${res}`);
}
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. * @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) { export function validateBody(schema: Schema) {
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {
const errs = validate(req.body, schema); const errs = validate(req.body, schema);
if (errs.length) return badRequest(res, "Validation failed", errs); if (errs.length) return badRequest(res, "Validation failed", errs);
next(); 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. * @return {(req: Request, res: Response, next: NextFunction) => void} A middleware function that validates the request parameters.
*/ */
export function validateParams(schema: Schema) { export function validateParams(schema: Schema) {
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {
const errs = validate(req.params, schema); const errs = validate(req.params, schema);
if (errs.length) return res.status(400).send({ error: "Validation failed", details: errs }); if (errs.length) return res.status(400).send({ error: "Validation failed", details: errs });
next(); 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. * @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) { export function validateQuery(schema: Schema) {
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {
const errs = validate(req.query, schema); const errs = validate(req.query, schema);
if (errs.length) return res.status(400).send({ error: "Validation failed", details: errs }); if (errs.length) return res.status(400).send({ error: "Validation failed", details: errs });
next(); next();
}; };
} }
+56 -49
View File
@@ -1,32 +1,30 @@
import {S3Service} from "../services/s3Service"; import { S3Service } from "../services/s3Service";
import multer from "multer" import multer from "multer";
import express from "express"; import express from "express";
import {asyncHandler} from "./middleware/asyncHandler"; import { asyncHandler } from "./middleware/asyncHandler";
import {badRequest, created, forbidden, notFound, ok} from "./utils/responses"; import { badRequest, created, forbidden, notFound, ok } from "./utils/responses";
import {vi} from "vitest"; import { vi } from "vitest";
vi.mock("../../src/services/db/UserService", () => ({ vi.mock("../../src/services/db/UserService", () => ({
UserService: { UserService: {
create: vi.fn() create: vi.fn(),
} },
})); }));
export class RestStorage { export class RestStorage {
constructor(private readonly s3Service: S3Service) {}
constructor(private readonly s3Service: S3Service) {
}
public createRouter() { public createRouter() {
const router = express.Router(); const router = express.Router();
const upload = multer({ const upload = multer({
storage: multer.memoryStorage(), storage: multer.memoryStorage(),
limits: {fileSize: 10 * 1024 * 1024}, limits: { fileSize: 10 * 1024 * 1024 },
}); });
router.post( router.post(
"/upload", "/upload",
upload.single('image'), upload.single("image"),
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
if (!req.file) { if (!req.file) {
return badRequest(res, "No file provided."); return badRequest(res, "No file provided.");
@@ -35,56 +33,65 @@ export class RestStorage {
const userId = req.payload.uuid; const userId = req.payload.uuid;
const objectKey = await this.s3Service.uploadFile(req.file, userId); 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) => { router.get(
const userId = req.payload.uuid; "/files",
const files = await this.s3Service.listFilesForUser(userId); 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) => { router.get(
const userId = req.payload.uuid; /\/files\/(.*)\/url$/,
const objectKey =req.params[0]; asyncHandler(async (req, res) => {
const userId = req.payload.uuid;
const objectKey = req.params[0];
console.log(userId); console.log(userId);
console.log(objectKey) console.log(objectKey);
if (!objectKey || !objectKey.startsWith(`user-${userId}`)) { if (!objectKey || !objectKey.startsWith(`user-${userId}`)) {
return forbidden(res); 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;
} }
} 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 router.delete(
const userId = req.payload.uuid; /\/files\/(.*)/,
const objectKey =req.params[0]; asyncHandler(async (req, res) => {
// <-- ÄNDERUNG HIER
const userId = req.payload.uuid;
const objectKey = req.params[0];
console.log(objectKey) console.log(objectKey);
if (!objectKey.startsWith(`user-${userId}/`)) { if (!objectKey.startsWith(`user-${userId}/`)) {
return forbidden(res); 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; return router;
} }
} }
+49 -34
View File
@@ -1,13 +1,12 @@
import express from "express"; import express from "express";
import {PasswordUtils} from "../utils/passwordUtils"; import { PasswordUtils } from "../utils/passwordUtils";
import {asyncHandler} from "./middleware/asyncHandler"; import { asyncHandler } from "./middleware/asyncHandler";
import {v, validateBody, validateParams} from "./middleware/validate"; import { v, validateBody, validateParams } from "./middleware/validate";
import {badRequest, ok} from "./utils/responses"; import { badRequest, ok } from "./utils/responses";
import {isAdmin} from "./middleware/isAdmin"; import { isAdmin } from "./middleware/isAdmin";
import {UserService} from "../services/db/UserService"; import { UserService } from "../services/db/UserService";
export class RestUser { export class RestUser {
private readonly userService: UserService; private readonly userService: UserService;
constructor(userService: UserService) { constructor(userService: UserService) {
@@ -17,27 +16,37 @@ export class RestUser {
public createRouter() { public createRouter() {
const router = express.Router(); const router = express.Router();
router.get("/", isAdmin(this.userService), asyncHandler(async (_req, res) => { router.get(
const users = await this.userService.getAllUsers(); "/",
return ok(res, {users}); isAdmin(this.userService),
})); asyncHandler(async (_req, res) => {
const users = await this.userService.getAllUsers();
return ok(res, { users });
})
);
router.get("/me", asyncHandler(async (req, res) => { router.get(
const user = await this.userService.getUserByUUID(req.payload.uuid); "/me",
return ok(res, user); asyncHandler(async (req, res) => {
})); const user = await this.userService.getUserByUUID(req.payload.uuid);
return ok(res, user);
})
);
router.put( router.put(
"/me/spotify", "/me/spotify",
validateBody({ validateBody({
accessToken: {required: true, validator: v.isString({nonEmpty: true})}, accessToken: { required: true, validator: v.isString({ nonEmpty: true }) },
refreshToken: {required: true, validator: v.isString({nonEmpty: true})}, refreshToken: { required: true, validator: v.isString({ nonEmpty: true }) },
scope: {required: true, validator: v.isString({nonEmpty: true})}, scope: { required: true, validator: v.isString({ nonEmpty: true }) },
expirationDate: {required: true, validator: v.isString({nonEmpty: true})}, expirationDate: { required: true, validator: v.isString({ nonEmpty: true }) },
}), }),
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const {accessToken, refreshToken, scope, expirationDate} = req.body as { const { accessToken, refreshToken, scope, expirationDate } = req.body as {
accessToken: string; refreshToken: string; scope: string; expirationDate: string; accessToken: string;
refreshToken: string;
scope: string;
expirationDate: string;
}; };
const spotifyConfig = { const spotifyConfig = {
@@ -47,24 +56,30 @@ export class RestUser {
expirationDate: new Date(expirationDate), expirationDate: new Date(expirationDate),
}; };
await this.userService.updateUserByUUID(req.payload.uuid, {spotifyConfig: spotifyConfig}); await this.userService.updateUserByUUID(req.payload.uuid, { spotifyConfig: spotifyConfig });
return ok(res, {message: "Spotify config changed successfully."}); return ok(res, { message: "Spotify config changed successfully." });
}) })
); );
router.delete("/me/spotify", asyncHandler(async (req, res) => { router.delete(
const updated = await this.userService.clearSpotifyConfigByUUID(req.payload.uuid); "/me/spotify",
return ok(res, {user: updated}); asyncHandler(async (req, res) => {
})); const updated = await this.userService.clearSpotifyConfigByUUID(req.payload.uuid);
return ok(res, { user: updated });
})
);
router.put( router.put(
"/me/password", "/me/password",
validateBody({ validateBody({
password: {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})}, passwordConfirmation: { required: true, validator: v.isString({ nonEmpty: true, min: 8 }) },
}), }),
asyncHandler(async (req, res) => { 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) { if (password !== passwordConfirmation) {
return badRequest(res, "Passwörter stimmen nicht überein"); return badRequest(res, "Passwörter stimmen nicht überein");
@@ -77,15 +92,15 @@ export class RestUser {
const newPassword = await PasswordUtils.hashPassword(password); const newPassword = await PasswordUtils.hashPassword(password);
await this.userService.updateUserByUUID(req.payload.uuid, {password: newPassword}); await this.userService.updateUserByUUID(req.payload.uuid, { password: newPassword });
return ok(res, {message: "Password changed successfully"}); return ok(res, { message: "Password changed successfully" });
}) })
); );
router.get( router.get(
"/:id", "/:id",
validateParams({ validateParams({
id: {required: true, validator: v.isString({nonEmpty: true})}, id: { required: true, validator: v.isString({ nonEmpty: true }) },
}), }),
isAdmin(this.userService), isAdmin(this.userService),
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
@@ -101,4 +116,4 @@ export class RestUser {
return router; return router;
} }
} }
+14 -10
View File
@@ -1,9 +1,9 @@
import express, { Router, Request, Response } from "express"; import express, { Router, Request, Response } from "express";
import { ExtendedWebSocketServer } from "../websocket"; import { ExtendedWebSocketServer } from "../websocket";
import { asyncHandler } from "./middleware/asyncHandler"; import { asyncHandler } from "./middleware/asyncHandler";
import {v, validateBody} from "./middleware/validate"; import { v, validateBody } from "./middleware/validate";
import { ok } from "./utils/responses"; import { ok } from "./utils/responses";
import {ExtendedWebSocket} from "../interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "../interfaces/extendedWebsocket";
export class RestWebSocket { export class RestWebSocket {
constructor(private webSocketServer: ExtendedWebSocketServer) {} constructor(private webSocketServer: ExtendedWebSocketServer) {}
@@ -36,7 +36,9 @@ export class RestWebSocket {
users: { users: {
required: true, required: true,
validator: (value: any) => 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 ? true
: "must be a non-empty array of strings", : "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) => { router.get(
const connectedClients = this.webSocketServer.getConnectedClients(); "/all-clients",
const result = Array.from(connectedClients).map((client: ExtendedWebSocket) => client.payload); asyncHandler(async (_req: Request, res: Response) => {
return ok(res, { result }); const connectedClients = this.webSocketServer.getConnectedClients();
})); const result = Array.from(connectedClients).map((client: ExtendedWebSocket) => client.payload);
return ok(res, { result });
})
);
return router; return router;
} }
} }
+13 -15
View File
@@ -1,13 +1,11 @@
import express from "express"; import express from "express";
import {asyncHandler} from "./middleware/asyncHandler"; import { asyncHandler } from "./middleware/asyncHandler";
import {validateBody, v} from "./middleware/validate"; import { validateBody, v } from "./middleware/validate";
import {ok, internalError} from "./utils/responses"; import { ok, internalError } from "./utils/responses";
import {SpotifyTokenService} from "../services/spotifyTokenService"; import { SpotifyTokenService } from "../services/spotifyTokenService";
export class SpotifyTokenGenerator { export class SpotifyTokenGenerator {
constructor(private spotifyTokenService: SpotifyTokenService) {}
constructor(private spotifyTokenService: SpotifyTokenService) {
}
public createRouter() { public createRouter() {
const router = express.Router(); const router = express.Router();
@@ -15,29 +13,29 @@ export class SpotifyTokenGenerator {
router.post( router.post(
"/token/refresh", "/token/refresh",
validateBody({ validateBody({
refreshToken: {required: true, validator: v.isString({nonEmpty: true})}, refreshToken: { required: true, validator: v.isString({ nonEmpty: true }) },
}), }),
asyncHandler(async (req, res) => { 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); const token = await this.spotifyTokenService.refreshToken(refreshToken);
return ok(res, {token}); return ok(res, { token });
}) })
); );
router.post( router.post(
"/token/generate", "/token/generate",
validateBody({ validateBody({
authCode: {required: true, validator: v.isString({nonEmpty: true})}, authCode: { required: true, validator: v.isString({ nonEmpty: true }) },
redirectUri: {required: true, validator: v.isUrl()}, redirectUri: { required: true, validator: v.isUrl() },
}), }),
asyncHandler(async (req, res) => { 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); 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; return router;
} }
} }
+16 -21
View File
@@ -2,39 +2,34 @@ import type { Response } from "express";
type ErrorDetails = unknown; type ErrorDetails = unknown;
function respondError( function respondError(res: Response, status: number, message: string, details?: ErrorDetails) {
res: Response, return res.status(status).send({
status: number, ok: false,
message: string, data: {
details?: ErrorDetails message,
) { details,
return res.status(status).send({ },
ok: false, });
data: {
message,
details,
},
});
} }
export function ok<T>(res: Response, data: T) { export function ok<T>(res: Response, data: T) {
return res.status(200).send({ ok: true, data }); return res.status(200).send({ ok: true, data });
} }
export function created<T>(res: Response, data: T) { export function created<T>(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) { 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) { 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) { 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) { 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) { 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) { 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) { export function internalError(res: Response, message = "Internal Server Error", details?: ErrorDetails) {
return respondError(res, 500, message, details); return respondError(res, 500, message, details);
} }
+51 -36
View File
@@ -1,27 +1,27 @@
import express, {Express, Request, Response, NextFunction} from "express"; import express, { Express, Request, Response, NextFunction } from "express";
import {Server as HttpServer} from "http"; import { Server as HttpServer } from "http";
import cors from "cors"; import cors from "cors";
import cookieParser from 'cookie-parser'; import cookieParser from "cookie-parser";
import {randomUUID} from "crypto"; import { randomUUID } from "crypto";
import {ExtendedWebSocketServer} from "./websocket"; import { ExtendedWebSocketServer } from "./websocket";
import {RestWebSocket} from "./rest/restWebSocket"; import { RestWebSocket } from "./rest/restWebSocket";
import {RestUser} from "./rest/restUser"; import { RestUser } from "./rest/restUser";
import {JwtTokenPropertiesExtractor} from "./rest/jwtTokenPropertiesExtractor"; import { JwtTokenPropertiesExtractor } from "./rest/jwtTokenPropertiesExtractor";
import {SpotifyTokenGenerator} from "./rest/spotifyTokenGenerator"; import { SpotifyTokenGenerator } from "./rest/spotifyTokenGenerator";
import {RestAuth} from "./rest/auth"; import { RestAuth } from "./rest/auth";
import {authLimiter, spotifyLimiter} from "./rest/middleware/rateLimit"; import { authLimiter, spotifyLimiter } from "./rest/middleware/rateLimit";
import {extractTokenFromCookie} from "./rest/middleware/extractTokenFromCookie"; import { extractTokenFromCookie } from "./rest/middleware/extractTokenFromCookie";
import {JwtAuthenticator} from "./utils/jwtAuthenticator"; import { JwtAuthenticator } from "./utils/jwtAuthenticator";
import {authenticateJwt} from "./rest/middleware/authenticateJwt"; import { authenticateJwt } from "./rest/middleware/authenticateJwt";
import {watchUserChanges} from "./db/models/userWatch"; import { watchUserChanges } from "./db/models/userWatch";
import {SpotifyPollingService} from "./services/spotifyPollingService"; import { SpotifyPollingService } from "./services/spotifyPollingService";
import {UserService} from "./services/db/UserService"; import { UserService } from "./services/db/UserService";
import {disconnectFromDatabase} from "./services/db/database.service"; import { disconnectFromDatabase } from "./services/db/database.service";
import {SpotifyTokenService} from "./services/spotifyTokenService"; import { SpotifyTokenService } from "./services/spotifyTokenService";
import {WeatherPollingService} from "./services/weatherPollingService"; import { WeatherPollingService } from "./services/weatherPollingService";
import {S3Service} from "./services/s3Service"; import { S3Service } from "./services/s3Service";
import {RestStorage} from "./rest/restStorage"; import { RestStorage } from "./rest/restStorage";
interface ServerDependencies { interface ServerDependencies {
userService: UserService; userService: UserService;
@@ -46,8 +46,10 @@ export class Server {
private httpServer: HttpServer | null = null; private httpServer: HttpServer | null = null;
private webSocketServer: ExtendedWebSocketServer | null = null; private webSocketServer: ExtendedWebSocketServer | null = null;
constructor(private readonly config: ServerConfig, constructor(
private readonly dependencies: ServerDependencies) { private readonly config: ServerConfig,
private readonly dependencies: ServerDependencies
) {
this.app = express(); this.app = express();
} }
@@ -58,10 +60,10 @@ export class Server {
spotifyTokenService, spotifyTokenService,
spotifyPollingService, spotifyPollingService,
weatherPollingService, weatherPollingService,
jwtAuthenticator jwtAuthenticator,
} = this.dependencies; } = this.dependencies;
await s3Service.ensureBucketExists() await s3Service.ensureBucketExists();
watchUserChanges(); watchUserChanges();
@@ -73,7 +75,13 @@ export class Server {
console.log(`Server is running on port ${this.config.port}`); 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(); this._setupGracefulShutdown();
@@ -93,15 +101,22 @@ export class Server {
private _setupMiddleware(): void { private _setupMiddleware(): void {
this.app.set("trust proxy", 1); this.app.set("trust proxy", 1);
this.app.use(cookieParser()); this.app.use(cookieParser());
this.app.use(cors({ this.app.use(
origin: this.config.cors.origin, cors({
credentials: this.config.cors.credentials, origin: this.config.cors.origin,
})); credentials: this.config.cors.credentials,
})
);
this.app.use(this._securityHeaders); 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 _authenticateJwt = authenticateJwt(jwtAuthenticator);
const restAuth = new RestAuth(userService, jwtAuthenticator); const restAuth = new RestAuth(userService, jwtAuthenticator);
@@ -110,7 +125,7 @@ export class Server {
const jwtTokenExtractor = new JwtTokenPropertiesExtractor(); const jwtTokenExtractor = new JwtTokenPropertiesExtractor();
const storage = new RestStorage(s3Service); 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()); this.app.use("/api/auth", authLimiter, restAuth.createRouter());
@@ -160,7 +175,7 @@ export class Server {
ok: false, ok: false,
data: { data: {
error: errorMessage, error: errorMessage,
...(statusCode >= 500 && {errorId: errorId}), ...(statusCode >= 500 && { errorId: errorId }),
}, },
}); });
}); });
@@ -173,4 +188,4 @@ export class Server {
process.exit(0); process.exit(0);
}); });
} }
} }
+16 -30
View File
@@ -1,12 +1,11 @@
import {connectToDatabase} from "./database.service"; import { connectToDatabase } from "./database.service";
import { UpdateQuery} from "mongoose"; import { UpdateQuery } from "mongoose";
import {CreateUserPayload, IUser, SpotifyConfig, UserModel} from "../../db/models/user"; import { CreateUserPayload, IUser, SpotifyConfig, UserModel } from "../../db/models/user";
export class UserService { export class UserService {
private static _instance: UserService; private static _instance: UserService;
private constructor() { private constructor() {}
}
public static async create(): Promise<UserService> { public static async create(): Promise<UserService> {
if (!this._instance) { if (!this._instance) {
@@ -23,15 +22,11 @@ export class UserService {
} }
public async updateUserByUUID(uuid: string, updates: Partial<IUser>): Promise<IUser | null> { public async updateUserByUUID(uuid: string, updates: Partial<IUser>): Promise<IUser | null> {
return await UserModel.findOneAndUpdate( return await UserModel.findOneAndUpdate({ uuid: uuid }, { $set: updates }, { new: true }).exec();
{ uuid: uuid },
{ $set: updates },
{ new: true }
).exec();
} }
public async getAllUsers(): Promise<IUser[]> { public async getAllUsers(): Promise<IUser[]> {
return await UserModel.find({}, {spotifyConfig: 0, lastState: 0}).exec(); return await UserModel.find({}, { spotifyConfig: 0, lastState: 0 }).exec();
} }
public async getUserById(id: string): Promise<IUser | null> { public async getUserById(id: string): Promise<IUser | null> {
@@ -39,25 +34,21 @@ export class UserService {
} }
public async getUserByUUID(uuid: string): Promise<IUser | null> { public async getUserByUUID(uuid: string): Promise<IUser | null> {
return await UserModel.findOne({uuid}).exec(); return await UserModel.findOne({ uuid }).exec();
} }
public async getUserByName(name: string): Promise<IUser | null> { public async getUserByName(name: string): Promise<IUser | null> {
return await UserModel.findOne({name}) return await UserModel.findOne({ name }).collation({ locale: "en", strength: 2 }).exec();
.collation({locale: "en", strength: 2})
.exec();
} }
public async getUserAuthByName(name: string): Promise<IUser | null> { public async getUserAuthByName(name: string): Promise<IUser | null> {
return await UserModel.findOne({name}) return await UserModel.findOne({ name }).collation({ locale: "en", strength: 2 }).select("+password").exec();
.collation({locale: "en", strength: 2})
.select("+password")
.exec();
} }
public async getSpotifyConfigByUUID(uuid: string): Promise<SpotifyConfig | undefined> { public async getSpotifyConfigByUUID(uuid: string): Promise<SpotifyConfig | undefined> {
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<IUser> { public async createUser(userData: CreateUserPayload): Promise<IUser> {
@@ -68,13 +59,12 @@ export class UserService {
delete userObject.password; delete userObject.password;
return userObject as IUser; return userObject as IUser;
} catch (error: any) { } catch (error: any) {
if (error.code === 11000 && error.keyPattern?.uuid) { if (error.code === 11000 && error.keyPattern?.uuid) {
throw new Error("User with that uuid already exists"); throw new Error("User with that uuid already exists");
} }
if (error.name === 'ValidationError') { if (error.name === "ValidationError") {
throw new Error(`ValidationError: ${error.message}`); throw new Error(`ValidationError: ${error.message}`);
} }
@@ -88,12 +78,8 @@ export class UserService {
} }
public async clearSpotifyConfigByUUID(uuid: string): Promise<IUser | null> { public async clearSpotifyConfigByUUID(uuid: string): Promise<IUser | null> {
return await UserModel.findOneAndUpdate( return await UserModel.findOneAndUpdate({ uuid }, { $unset: { spotifyConfig: 1 } } as UpdateQuery<IUser>, {
{ uuid }, new: true,
{ $unset: { spotifyConfig: 1 } } as UpdateQuery<IUser>, }).exec();
{ new: true }
).exec();
} }
} }
+7 -7
View File
@@ -18,7 +18,7 @@ const connectWithRetry = async (dbName: string, dbConnString: string): Promise<v
await mongoose.connect(dbConnString, options); await mongoose.connect(dbConnString, options);
} catch (error) { } catch (error) {
console.error("Failed to connect to MongoDB. Retrying in 5 seconds...", error); console.error("Failed to connect to MongoDB. Retrying in 5 seconds...", error);
await new Promise<void>(resolve => setTimeout(resolve, 5000)); await new Promise<void>((resolve) => setTimeout(resolve, 5000));
return connectWithRetry(dbName, dbConnString); return connectWithRetry(dbName, dbConnString);
} }
}; };
@@ -34,19 +34,19 @@ export async function connectToDatabase(dbName: string, dbConnString: string): P
return; return;
} }
mongoose.connection.on('connected', () => { mongoose.connection.on("connected", () => {
isConnected = true; isConnected = true;
console.log('Mongoose connected to DB.'); console.log("Mongoose connected to DB.");
}); });
mongoose.connection.on('disconnected', () => { mongoose.connection.on("disconnected", () => {
isConnected = false; 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; isConnected = false;
console.error('Mongoose connection error:', err); console.error("Mongoose connection error:", err);
}); });
await connectWithRetry(dbName, dbConnString); await connectWithRetry(dbName, dbConnString);
+3 -3
View File
@@ -1,7 +1,7 @@
import OpenWeatherAPI from "openweather-api-node" import OpenWeatherAPI from "openweather-api-node";
function getWeatherInstance(): OpenWeatherAPI { function getWeatherInstance(): OpenWeatherAPI {
return new OpenWeatherAPI({ return new OpenWeatherAPI({
key: process.env.OWM_API_KEY, key: process.env.OWM_API_KEY,
}); });
} }
@@ -9,6 +9,6 @@ function getWeatherInstance(): OpenWeatherAPI {
export async function getCurrentWeather(location: string) { export async function getCurrentWeather(location: string) {
return getWeatherInstance().getCurrent({ return getWeatherInstance().getCurrent({
locationName: location, locationName: location,
units: "metric" units: "metric",
}); });
} }
+14 -10
View File
@@ -2,10 +2,12 @@ import {
S3Client, S3Client,
CreateBucketCommand, CreateBucketCommand,
PutObjectCommand, PutObjectCommand,
GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand GetObjectCommand,
ListObjectsV2Command,
DeleteObjectCommand,
} from "@aws-sdk/client-s3"; } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { randomUUID } from 'crypto'; import { randomUUID } from "crypto";
export interface S3ClientConfig { export interface S3ClientConfig {
endpoint: string; endpoint: string;
@@ -51,7 +53,7 @@ export class S3Service {
await this.client.send(new CreateBucketCommand({ Bucket: this.bucketName })); await this.client.send(new CreateBucketCommand({ Bucket: this.bucketName }));
console.log(`Bucket "${this.bucketName}" created successfully or already existed.`); console.log(`Bucket "${this.bucketName}" created successfully or already existed.`);
} catch (err: any) { } catch (err: any) {
if (err.name === 'BucketAlreadyOwnedByYou' || err.name === 'BucketAlreadyExists') { if (err.name === "BucketAlreadyOwnedByYou" || err.name === "BucketAlreadyExists") {
console.log(`Bucket "${this.bucketName}" already exists.`); console.log(`Bucket "${this.bucketName}" already exists.`);
} else { } else {
throw err; throw err;
@@ -60,7 +62,7 @@ export class S3Service {
} }
async uploadFile(file: Express.Multer.File, userId: string): Promise<string> { async uploadFile(file: Express.Multer.File, userId: string): Promise<string> {
const fileExtension = file.originalname.split('.').pop(); const fileExtension = file.originalname.split(".").pop();
const objectKey = `user-${userId}/${randomUUID()}.${fileExtension}`; const objectKey = `user-${userId}/${randomUUID()}.${fileExtension}`;
const command = new PutObjectCommand({ const command = new PutObjectCommand({
@@ -74,7 +76,7 @@ export class S3Service {
return objectKey; return objectKey;
} }
async listFilesForUser(userId: string): Promise<{ key: string, lastModified: Date }[]> { async listFilesForUser(userId: string): Promise<{ key: string; lastModified: Date }[]> {
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: this.bucketName, Bucket: this.bucketName,
Prefix: `user-${userId}/`, Prefix: `user-${userId}/`,
@@ -82,10 +84,12 @@ export class S3Service {
const response = await this.client.send(command); const response = await this.client.send(command);
return response.Contents?.map(item => ({ return (
key: item.Key!, response.Contents?.map((item) => ({
lastModified: item.LastModified!, key: item.Key!,
})) || []; lastModified: item.LastModified!,
})) || []
);
} }
async deleteFile(objectKey: string): Promise<void> { async deleteFile(objectKey: string): Promise<void> {
@@ -106,4 +110,4 @@ export class S3Service {
return await getSignedUrl(this.client, command, { expiresIn }); return await getSignedUrl(this.client, command, { expiresIn });
} }
} }
+5 -5
View File
@@ -1,5 +1,5 @@
import axios from "axios"; import axios from "axios";
import {CurrentlyPlaying} from "../interfaces/CurrentlyPlaying"; import { CurrentlyPlaying } from "../interfaces/CurrentlyPlaying";
export class SpotifyApiService { export class SpotifyApiService {
private readonly apiUrl = "https://api.spotify.com/v1"; private readonly apiUrl = "https://api.spotify.com/v1";
@@ -8,11 +8,11 @@ export class SpotifyApiService {
try { try {
const response = await axios.get<CurrentlyPlaying>(`${this.apiUrl}/me/player/currently-playing`, { const response = await axios.get<CurrentlyPlaying>(`${this.apiUrl}/me/player/currently-playing`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
}, },
params: { params: {
additional_types: "episode" additional_types: "episode",
} },
}); });
if (response.status === 204) { if (response.status === 204) {
@@ -28,4 +28,4 @@ export class SpotifyApiService {
throw error; throw error;
} }
} }
} }
+7 -9
View File
@@ -2,8 +2,8 @@ import { appEventBus, SPOTIFY_STATE_UPDATED_EVENT } from "../utils/eventBus";
import { SpotifyApiService } from "./spotifyApiService"; import { SpotifyApiService } from "./spotifyApiService";
import { IUser } from "../db/models/user"; import { IUser } from "../db/models/user";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import {UserService} from "./db/UserService"; import { UserService } from "./db/UserService";
import {SpotifyTokenService} from "./spotifyTokenService"; import { SpotifyTokenService } from "./spotifyTokenService";
const userStateCache = new Map<string, any>(); const userStateCache = new Map<string, any>();
const activePolls = new Map<string, NodeJS.Timeout>(); const activePolls = new Map<string, NodeJS.Timeout>();
@@ -12,7 +12,7 @@ export class SpotifyPollingService {
constructor( constructor(
private readonly userService: UserService, private readonly userService: UserService,
private readonly spotifyApiService: SpotifyApiService, private readonly spotifyApiService: SpotifyApiService,
private readonly spotifyTokenService: SpotifyTokenService, private readonly spotifyTokenService: SpotifyTokenService
) {} ) {}
public startPollingForUser(user: IUser): void { public startPollingForUser(user: IUser): void {
@@ -65,11 +65,10 @@ export class SpotifyPollingService {
userStateCache.set(uuid, currentState); userStateCache.set(uuid, currentState);
appEventBus.emit(SPOTIFY_STATE_UPDATED_EVENT, { uuid, state: currentState }); appEventBus.emit(SPOTIFY_STATE_UPDATED_EVENT, { uuid, state: currentState });
} }
} catch (error) { } catch (error) {
if (error instanceof AxiosError && error.response) { if (error instanceof AxiosError && error.response) {
if (error.response.status === 429) { 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.`); console.warn(`[SpotifyPolling] Rate limit for ${uuid}. Pausing for ${retryAfter}s.`);
this._pausePolling(uuid, retryAfter * 1000); this._pausePolling(uuid, retryAfter * 1000);
} else if (error.response.status === 401) { } else if (error.response.status === 401) {
@@ -86,8 +85,7 @@ export class SpotifyPollingService {
if (!currentState && !lastState) return false; if (!currentState && !lastState) return false;
if (!currentState || !lastState) return true; if (!currentState || !lastState) return true;
return lastState.item?.id !== currentState.item?.id || return lastState.item?.id !== currentState.item?.id || lastState.is_playing !== currentState.is_playing;
lastState.is_playing !== currentState.is_playing;
} }
private _pausePolling(uuid: string, durationMs: number): void { private _pausePolling(uuid: string, durationMs: number): void {
@@ -96,10 +94,10 @@ export class SpotifyPollingService {
activePolls.delete(uuid); activePolls.delete(uuid);
setTimeout(() => { setTimeout(() => {
console.log(`[SpotifyPolling] Resuming polling for ${uuid}.`); console.log(`[SpotifyPolling] Resuming polling for ${uuid}.`);
this.userService.getUserByUUID(uuid).then(user => { this.userService.getUserByUUID(uuid).then((user) => {
if (user) this.startPollingForUser(user); if (user) this.startPollingForUser(user);
}); });
}, durationMs); }, durationMs);
} }
} }
} }
+14 -20
View File
@@ -1,43 +1,37 @@
import axios from "axios"; import axios from "axios";
import {OAuthTokenResponse} from "../interfaces/OAuthTokenResponse"; import { OAuthTokenResponse } from "../interfaces/OAuthTokenResponse";
const url = "https://accounts.spotify.com/api/token"; const url = "https://accounts.spotify.com/api/token";
export class SpotifyTokenService { 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) { public async refreshToken(refreshToken: string) {
console.log("refreshToken") console.log("refreshToken");
const response = await axios.post( const response = await axios.post(url, `grant_type=refresh_token&refresh_token=${refreshToken}`, {
url, headers: {
`grant_type=refresh_token&refresh_token=${refreshToken}`, "Content-Type": "application/x-www-form-urlencoded",
{ Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")}`,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${this.clientId}:${this.clientSecret}`,
).toString("base64")}`,
},
}, },
); });
return response.data as OAuthTokenResponse; return response.data as OAuthTokenResponse;
} }
public async generateToken(authorizationCode: string, redirectUri: string) { public async generateToken(authorizationCode: string, redirectUri: string) {
console.log("generateToken") console.log("generateToken");
const response = await axios.post( const response = await axios.post(
url, url,
`grant_type=authorization_code&code=${authorizationCode}&redirect_uri=${redirectUri}`, `grant_type=authorization_code&code=${authorizationCode}&redirect_uri=${redirectUri}`,
{ {
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from( Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")}`,
`${this.clientId}:${this.clientSecret}`,
).toString("base64")}`,
}, },
}, }
); );
console.log(response.data); console.log(response.data);
+4 -6
View File
@@ -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 { getCurrentWeather } from "./owmApiService";
import {IUser} from "../db/models/user"; import { IUser } from "../db/models/user";
export class WeatherPollingService { export class WeatherPollingService {
private readonly activeLocationPolls: Map<string, NodeJS.Timeout>; private readonly activeLocationPolls: Map<string, NodeJS.Timeout>;
private readonly locationSubscriptions: Map<string, Set<string>>; private readonly locationSubscriptions: Map<string, Set<string>>;
private readonly weatherCache: Map<string, any>; private readonly weatherCache: Map<string, any>;
@@ -53,7 +51,7 @@ export class WeatherPollingService {
this.weatherCache.delete(location); this.weatherCache.delete(location);
} }
} }
this.userLocationCache.delete(uuid) this.userLocationCache.delete(uuid);
} }
private _startPollingForLocation(location: string): void { private _startPollingForLocation(location: string): void {
@@ -101,4 +99,4 @@ export class WeatherPollingService {
this.subscribeUser(uuid, newLocation); this.subscribeUser(uuid, newLocation);
} }
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import {DecodedToken} from "../interfaces/decodedToken"; import { DecodedToken } from "../interfaces/decodedToken";
declare global { declare global {
declare namespace Express { declare namespace Express {
+4 -4
View File
@@ -1,6 +1,6 @@
import { EventEmitter } from 'events'; import { EventEmitter } from "events";
export const appEventBus = new EventEmitter(); export const appEventBus = new EventEmitter();
export const USER_UPDATED_EVENT = 'user:updated'; export const USER_UPDATED_EVENT = "user:updated";
export const SPOTIFY_STATE_UPDATED_EVENT = 'spotify:updated'; export const SPOTIFY_STATE_UPDATED_EVENT = "spotify:updated";
export const WEATHER_STATE_UPDATED_EVENT = 'weather:updated'; export const WEATHER_STATE_UPDATED_EVENT = "weather:updated";
+15 -15
View File
@@ -2,23 +2,23 @@ import jwt from "jsonwebtoken";
import { DecodedToken } from "../interfaces/decodedToken"; import { DecodedToken } from "../interfaces/decodedToken";
export class JwtAuthenticator { export class JwtAuthenticator {
constructor(private secret: string) {} constructor(private secret: string) {}
public verifyToken(token: string | undefined): DecodedToken | null { public verifyToken(token: string | undefined): DecodedToken | null {
if (!token) { if (!token) {
return null; return null;
}
try {
return jwt.verify(token, this.secret) as DecodedToken;
} catch (error) {
console.error("Error while verifying token:", error);
}
return null;
} }
try { public generateToken(payload: DecodedToken): string {
return jwt.verify(token, this.secret) as DecodedToken; return jwt.sign(payload, this.secret);
} catch (error) {
console.error("Error while verifying token:", error);
} }
return null;
}
public generateToken(payload: DecodedToken): string {
return jwt.sign(payload, this.secret);
}
} }
+7 -10
View File
@@ -6,9 +6,7 @@ export type ValidationResult = {
}; };
export class PasswordUtils { export class PasswordUtils {
private constructor() {}
private constructor() {
}
public static async hashPassword(password: string): Promise<string> { public static async hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 10); return bcrypt.hash(password, 10);
@@ -26,26 +24,25 @@ export class PasswordUtils {
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
if (password.length < minLength) { 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) { 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) { if (!hasLowerCase) {
return {valid: false, message: "Passwort muss mindestens einen Kleinbuchstaben enthalten."}; return { valid: false, message: "Passwort muss mindestens einen Kleinbuchstaben enthalten." };
} }
if (!hasNumber) { if (!hasNumber) {
return {valid: false, message: "Passwort muss mindestens eine Zahl enthalten."}; return { valid: false, message: "Passwort muss mindestens eine Zahl enthalten." };
} }
if (!hasSpecialChar) { 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." };
} }
} }
+14 -20
View File
@@ -1,30 +1,24 @@
import "dotenv/config"; import "dotenv/config";
import {IncomingMessage} from "node:http"; import { IncomingMessage } from "node:http";
import {ExtendedIncomingMessage} from "../interfaces/extendedIncomingMessage"; import { ExtendedIncomingMessage } from "../interfaces/extendedIncomingMessage";
import {JwtAuthenticator} from "./jwtAuthenticator"; import { JwtAuthenticator } from "./jwtAuthenticator";
export function verifyClient( export function verifyClient(
request: IncomingMessage, request: IncomingMessage,
jwtAuthenticator: JwtAuthenticator, 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)); const token = jwtAuthenticator.verifyToken(request.headers["authorization"]?.slice("Bearer ".length));
if (!token) { if (!token) {
reject(request, callback); reject(request, callback);
} else { } else {
(request as ExtendedIncomingMessage).payload = token; (request as ExtendedIncomingMessage).payload = token;
callback(true); callback(true);
} }
} }
const reject = ( const reject = (request: IncomingMessage, callback: (res: boolean, code?: number, message?: string) => void) => {
request: IncomingMessage, console.log("Connection refused", `${request.socket.remoteAddress}:${request.socket.remotePort}`);
callback: (res: boolean, code?: number, message?: string) => void, callback(false, 401, "Unauthorized");
) => {
console.log(
"Connection refused",
`${request.socket.remoteAddress}:${request.socket.remotePort}`,
);
callback(false, 401, "Unauthorized");
}; };
@@ -1,3 +1 @@
export interface NoData{ export interface NoData {}
}
@@ -1,4 +1,4 @@
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket";
export abstract class CustomWebsocketEvent<T = any> { export abstract class CustomWebsocketEvent<T = any> {
abstract event: string; abstract event: string;
@@ -8,5 +8,4 @@ export abstract class CustomWebsocketEvent<T = any> {
public constructor(ws: ExtendedWebSocket) { public constructor(ws: ExtendedWebSocket) {
this.ws = ws; this.ws = ws;
} }
} }
@@ -1,10 +1,10 @@
import {WebsocketEventType} from "./websocketEventType"; import { WebsocketEventType } from "./websocketEventType";
import {CustomWebsocketEvent} from "./customWebsocketEvent"; import { CustomWebsocketEvent } from "./customWebsocketEvent";
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket";
interface ErrorData { interface ErrorData {
message: string; message: string;
traceback: string traceback: string;
} }
export class ErrorEvent extends CustomWebsocketEvent<ErrorData> { export class ErrorEvent extends CustomWebsocketEvent<ErrorData> {
@@ -17,5 +17,5 @@ export class ErrorEvent extends CustomWebsocketEvent<ErrorData> {
handler = async (data: ErrorData) => { handler = async (data: ErrorData) => {
console.warn("Error message received", data.message); console.warn("Error message received", data.message);
console.warn("Traceback", data.traceback); console.warn("Traceback", data.traceback);
} };
} }
@@ -1,18 +1,20 @@
import {CustomWebsocketEvent} from "./customWebsocketEvent"; import { CustomWebsocketEvent } from "./customWebsocketEvent";
import {WebsocketEventType} from "./websocketEventType"; import { WebsocketEventType } from "./websocketEventType";
import {NoData} from "./NoData"; import { NoData } from "./NoData";
export class GetSettingsEvent extends CustomWebsocketEvent<NoData> { export class GetSettingsEvent extends CustomWebsocketEvent<NoData> {
event = WebsocketEventType.GET_SETTINGS; event = WebsocketEventType.GET_SETTINGS;
handler = async () => { handler = async () => {
console.log("Getting settings"); console.log("Getting settings");
this.ws.send(JSON.stringify({ this.ws.send(
type: "SETTINGS", JSON.stringify({
payload: { type: "SETTINGS",
timezone: this.ws.user.timezone, payload: {
}, timezone: this.ws.user.timezone,
}), {binary: false}); },
} }),
{ binary: false }
);
};
} }
@@ -1,14 +1,16 @@
import { SpotifyPollingService } from "../../../services/spotifyPollingService"; import { SpotifyPollingService } from "../../../services/spotifyPollingService";
import {WebsocketEventType} from "./websocketEventType"; import { WebsocketEventType } from "./websocketEventType";
import {NoData} from "./NoData"; import { NoData } from "./NoData";
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket";
import {CustomWebsocketEvent} from "./customWebsocketEvent"; import { CustomWebsocketEvent } from "./customWebsocketEvent";
export class GetSpotifyUpdatesEvent extends CustomWebsocketEvent<NoData> { export class GetSpotifyUpdatesEvent extends CustomWebsocketEvent<NoData> {
event = WebsocketEventType.GET_SPOTIFY_UPDATE; event = WebsocketEventType.GET_SPOTIFY_UPDATE;
constructor(ws: ExtendedWebSocket, private spotifyPollingService: SpotifyPollingService) { constructor(
ws: ExtendedWebSocket,
private spotifyPollingService: SpotifyPollingService
) {
super(ws); super(ws);
} }
@@ -17,5 +19,5 @@ export class GetSpotifyUpdatesEvent extends CustomWebsocketEvent<NoData> {
if (this.ws.user) { if (this.ws.user) {
this.spotifyPollingService.startPollingForUser(this.ws.user); this.spotifyPollingService.startPollingForUser(this.ws.user);
} }
} };
} }
@@ -1,6 +1,6 @@
import {CustomWebsocketEvent} from "./customWebsocketEvent"; import { CustomWebsocketEvent } from "./customWebsocketEvent";
import {WebsocketEventType} from "./websocketEventType"; import { WebsocketEventType } from "./websocketEventType";
import {NoData} from "./NoData"; import { NoData } from "./NoData";
export class GetStateEvent extends CustomWebsocketEvent<NoData> { export class GetStateEvent extends CustomWebsocketEvent<NoData> {
event = WebsocketEventType.GET_STATE; event = WebsocketEventType.GET_STATE;
@@ -11,6 +11,6 @@ export class GetStateEvent extends CustomWebsocketEvent<NoData> {
type: "STATE", type: "STATE",
payload: this.ws.user.lastState, payload: this.ws.user.lastState,
}; };
this.ws.send(JSON.stringify(messageToSend), {binary: false}); this.ws.send(JSON.stringify(messageToSend), { binary: false });
} };
} }
@@ -1,15 +1,14 @@
import {CustomWebsocketEvent} from "./customWebsocketEvent"; import { CustomWebsocketEvent } from "./customWebsocketEvent";
import {WebsocketEventType} from "./websocketEventType"; import { WebsocketEventType } from "./websocketEventType";
import {NoData} from "./NoData"; import { NoData } from "./NoData";
import {WeatherPollingService} from "../../../services/weatherPollingService"; import { WeatherPollingService } from "../../../services/weatherPollingService";
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket";
export class GetWeatherUpdatesEvent extends CustomWebsocketEvent<NoData> { export class GetWeatherUpdatesEvent extends CustomWebsocketEvent<NoData> {
event = WebsocketEventType.GET_WEATHER_UPDATES; event = WebsocketEventType.GET_WEATHER_UPDATES;
private readonly weatherPollingService: WeatherPollingService; private readonly weatherPollingService: WeatherPollingService;
constructor(ws: ExtendedWebSocket, weatherPollingService:WeatherPollingService) { constructor(ws: ExtendedWebSocket, weatherPollingService: WeatherPollingService) {
super(ws); super(ws);
this.weatherPollingService = weatherPollingService; this.weatherPollingService = weatherPollingService;
} }
@@ -19,6 +18,5 @@ export class GetWeatherUpdatesEvent extends CustomWebsocketEvent<NoData> {
if (user?.location && user.uuid) { if (user?.location && user.uuid) {
this.weatherPollingService.subscribeUser(user.uuid, user.location); this.weatherPollingService.subscribeUser(user.uuid, user.location);
} }
} };
} }
@@ -1,11 +1,10 @@
import {CustomWebsocketEvent} from "./customWebsocketEvent"; import { CustomWebsocketEvent } from "./customWebsocketEvent";
import {WebsocketEventType} from "./websocketEventType"; import { WebsocketEventType } from "./websocketEventType";
import {NoData} from "./NoData"; import { NoData } from "./NoData";
import {SpotifyPollingService} from "../../../services/spotifyPollingService"; import { SpotifyPollingService } from "../../../services/spotifyPollingService";
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket";
export class StopSpotifyUpdatesEvent extends CustomWebsocketEvent<NoData> { export class StopSpotifyUpdatesEvent extends CustomWebsocketEvent<NoData> {
event = WebsocketEventType.STOP_SPOTIFY_UPDATES; event = WebsocketEventType.STOP_SPOTIFY_UPDATES;
private readonly spotifyPollingService: SpotifyPollingService; private readonly spotifyPollingService: SpotifyPollingService;
@@ -25,6 +24,5 @@ export class StopSpotifyUpdatesEvent extends CustomWebsocketEvent<NoData> {
} else { } else {
console.warn("Could not stop Spotify polling: No UUID found on WebSocket payload."); console.warn("Could not stop Spotify polling: No UUID found on WebSocket payload.");
} }
} };
} }
@@ -1,15 +1,14 @@
import {CustomWebsocketEvent} from "./customWebsocketEvent"; import { CustomWebsocketEvent } from "./customWebsocketEvent";
import {WebsocketEventType} from "./websocketEventType"; import { WebsocketEventType } from "./websocketEventType";
import {NoData} from "./NoData"; import { NoData } from "./NoData";
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket";
import {WeatherPollingService} from "../../../services/weatherPollingService"; import { WeatherPollingService } from "../../../services/weatherPollingService";
export class StopWeatherUpdatesEvent extends CustomWebsocketEvent<NoData> { export class StopWeatherUpdatesEvent extends CustomWebsocketEvent<NoData> {
event = WebsocketEventType.STOP_WEATHER_UPDATES; event = WebsocketEventType.STOP_WEATHER_UPDATES;
private readonly weatherPollingService: WeatherPollingService; private readonly weatherPollingService: WeatherPollingService;
constructor(ws:ExtendedWebSocket, weatherPollingService:WeatherPollingService) { constructor(ws: ExtendedWebSocket, weatherPollingService: WeatherPollingService) {
super(ws); super(ws);
this.weatherPollingService = weatherPollingService; this.weatherPollingService = weatherPollingService;
} }
@@ -20,6 +19,5 @@ export class StopWeatherUpdatesEvent extends CustomWebsocketEvent<NoData> {
if (user?.location && user.uuid) { if (user?.location && user.uuid) {
this.weatherPollingService.unsubscribeUser(user.uuid, user.location); this.weatherPollingService.unsubscribeUser(user.uuid, user.location);
} }
} };
} }
@@ -1,16 +1,15 @@
import {WebsocketEventType} from "./websocketEventType"; import { WebsocketEventType } from "./websocketEventType";
import {CustomWebsocketEvent} from "./customWebsocketEvent"; import { CustomWebsocketEvent } from "./customWebsocketEvent";
import {IUser} from "../../../db/models/user"; import { IUser } from "../../../db/models/user";
export class UpdateUserSingleEvent extends CustomWebsocketEvent<IUser> { export class UpdateUserSingleEvent extends CustomWebsocketEvent<IUser> {
event = WebsocketEventType.UPDATE_USER_SINGLE; event = WebsocketEventType.UPDATE_USER_SINGLE;
handler = async (data: IUser) => { handler = async (data: IUser) => {
console.log("Updating user") console.log("Updating user");
if (data) { if (data) {
this.ws.user = data; this.ws.user = data;
console.log("User updated") console.log("User updated");
} }
} };
} }
@@ -1,17 +1,21 @@
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "../../../interfaces/extendedWebsocket";
import {GetSettingsEvent} from "./getSettingsEvent"; import { GetSettingsEvent } from "./getSettingsEvent";
import {ErrorEvent} from "./errorEvent"; import { ErrorEvent } from "./errorEvent";
import {GetSpotifyUpdatesEvent} from "./getSpotifyUpdatesEvent"; import { GetSpotifyUpdatesEvent } from "./getSpotifyUpdatesEvent";
import {GetStateEvent} from "./getStateEvent"; import { GetStateEvent } from "./getStateEvent";
import {GetWeatherUpdatesEvent} from "./getWeatherUpdatesEvent"; import { GetWeatherUpdatesEvent } from "./getWeatherUpdatesEvent";
import {StopSpotifyUpdatesEvent} from "./stopSpotifyUpdatesEvent"; import { StopSpotifyUpdatesEvent } from "./stopSpotifyUpdatesEvent";
import {StopWeatherUpdatesEvent} from "./stopWeatherUpdatesEvent"; import { StopWeatherUpdatesEvent } from "./stopWeatherUpdatesEvent";
import { UpdateUserSingleEvent} from "./updateUserEvent"; import { UpdateUserSingleEvent } from "./updateUserEvent";
import {CustomWebsocketEvent} from "./customWebsocketEvent"; import { CustomWebsocketEvent } from "./customWebsocketEvent";
import {SpotifyPollingService} from "../../../services/spotifyPollingService"; import { SpotifyPollingService } from "../../../services/spotifyPollingService";
import {WeatherPollingService} from "../../../services/weatherPollingService"; 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 [ return [
new GetStateEvent(ws), new GetStateEvent(ws),
new GetSettingsEvent(ws), new GetSettingsEvent(ws),
@@ -20,6 +24,6 @@ export function getEventListeners(ws: ExtendedWebSocket, spotifyPollingService:
new GetWeatherUpdatesEvent(ws, weatherPollingService), new GetWeatherUpdatesEvent(ws, weatherPollingService),
new StopWeatherUpdatesEvent(ws, weatherPollingService), new StopWeatherUpdatesEvent(ws, weatherPollingService),
new UpdateUserSingleEvent(ws), new UpdateUserSingleEvent(ws),
new ErrorEvent(ws) new ErrorEvent(ws),
]; ];
} }
+17 -16
View File
@@ -1,12 +1,15 @@
import {ExtendedWebSocket} from "../../interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "../../interfaces/extendedWebsocket";
import {CustomWebsocketEvent} from "./websocketCustomEvents/customWebsocketEvent"; import { CustomWebsocketEvent } from "./websocketCustomEvents/customWebsocketEvent";
import {getEventListeners} from "./websocketCustomEvents/websocketEventUtils"; import { getEventListeners } from "./websocketCustomEvents/websocketEventUtils";
import {SpotifyPollingService} from "../../services/spotifyPollingService"; import { SpotifyPollingService } from "../../services/spotifyPollingService";
import {WeatherPollingService} from "../../services/weatherPollingService"; import { WeatherPollingService } from "../../services/weatherPollingService";
export class WebsocketEventHandler { 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() { public enableErrorEvent() {
this.webSocket.on("error", console.error); this.webSocket.on("error", console.error);
@@ -31,15 +34,14 @@ export class WebsocketEventHandler {
public enableMessageEvent() { public enableMessageEvent() {
this.webSocket.on("message", (data) => { this.webSocket.on("message", (data) => {
const message = data.toString(); const message = data.toString();
const messageJson = JSON.parse(message); const messageJson = JSON.parse(message);
const {type} = messageJson; const { type } = messageJson;
console.log("Received message:", message); console.log("Received message:", message);
// emit event to the custom event handler // emit event to the custom event handler
this.webSocket.emit(type, messageJson); this.webSocket.emit(type, messageJson);
} });
);
} }
public registerCustomEvents() { public registerCustomEvents() {
@@ -50,5 +52,4 @@ export class WebsocketEventHandler {
private registerCustomEvent(customWebsocketEvent: CustomWebsocketEvent) { private registerCustomEvent(customWebsocketEvent: CustomWebsocketEvent) {
this.webSocket.on(customWebsocketEvent.event, customWebsocketEvent.handler.bind(customWebsocketEvent)); this.webSocket.on(customWebsocketEvent.event, customWebsocketEvent.handler.bind(customWebsocketEvent));
} }
} }
@@ -1,39 +1,39 @@
import {ExtendedWebSocket} from "../../interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "../../interfaces/extendedWebsocket";
import {ExtendedIncomingMessage} from "../../interfaces/extendedIncomingMessage"; import { ExtendedIncomingMessage } from "../../interfaces/extendedIncomingMessage";
import {Server as WebSocketServer} from "ws"; import { Server as WebSocketServer } from "ws";
import {heartbeat} from "./websocketServerHeartbeatInterval"; import { heartbeat } from "./websocketServerHeartbeatInterval";
import {UserService} from "../../services/db/UserService"; import { UserService } from "../../services/db/UserService";
export class WebsocketServerEventHandler { export class WebsocketServerEventHandler {
private readonly heartbeat: () => void; private readonly heartbeat: () => void;
private readonly userService: UserService; private readonly userService: UserService;
constructor(private webSocketServer: WebSocketServer, userService: UserService) { constructor(
private webSocketServer: WebSocketServer,
userService: UserService
) {
this.heartbeat = heartbeat(this.webSocketServer); this.heartbeat = heartbeat(this.webSocketServer);
this.userService = userService; this.userService = userService;
} }
public enableConnectionEvent( public enableConnectionEvent(callback: (ws: ExtendedWebSocket, request: ExtendedIncomingMessage) => void) {
callback: (ws: ExtendedWebSocket, request: ExtendedIncomingMessage) => void, this.webSocketServer.on("connection", async (ws: ExtendedWebSocket, request: ExtendedIncomingMessage) => {
) { const user = await this.userService.getUserByUUID(request.payload.uuid);
this.webSocketServer.on(
"connection",
async (ws: ExtendedWebSocket, request: ExtendedIncomingMessage) => {
const user = await this.userService.getUserByUUID(request.payload.uuid);
if (!user) { ws.terminate(); return; } if (!user) {
ws.user = user; ws.terminate();
return;
}
ws.user = user;
// first: map the payload from the request to the ws object (is payloed needed anymore?) // first: map the payload from the request to the ws object (is payloed needed anymore?)
ws.payload = request.payload; ws.payload = request.payload;
// second: set the isAlive flag to true // second: set the isAlive flag to true
ws.isAlive = true; ws.isAlive = true;
// last: call the callback function
// last: call the callback function callback(ws, request);
callback(ws, request); });
},
);
} }
public enableHeartbeat(interval: number) { public enableHeartbeat(interval: number) {
@@ -2,21 +2,13 @@ import { WebSocket, WebSocketServer } from "ws";
import { DecodedToken } from "../../interfaces/decodedToken"; import { DecodedToken } from "../../interfaces/decodedToken";
export function heartbeat(wss: WebSocketServer) { export function heartbeat(wss: WebSocketServer) {
return () => { return () => {
wss.clients.forEach( wss.clients.forEach((ws: WebSocket & { isAlive?: boolean; payload?: DecodedToken }) => {
(ws: WebSocket & { isAlive?: boolean; payload?: DecodedToken }) => { console.log(new Date().toLocaleString("de-DE") + ":" + ws.payload?.username + ": isAlive: " + ws.isAlive);
console.log( if (!ws.isAlive) return ws.terminate();
new Date().toLocaleString("de-DE") +
":" +
ws.payload?.username +
": isAlive: " +
ws.isAlive,
);
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false; ws.isAlive = false;
ws.ping(); ws.ping();
}, });
); };
};
} }
+44 -30
View File
@@ -1,21 +1,21 @@
import {Server} from "http"; import { Server } from "http";
import {Server as WebSocketServer, WebSocket} from "ws"; import { Server as WebSocketServer, WebSocket } from "ws";
import {verifyClient} from "./utils/verifyClient"; import { verifyClient } from "./utils/verifyClient";
import {ExtendedWebSocket} from "./interfaces/extendedWebsocket"; import { ExtendedWebSocket } from "./interfaces/extendedWebsocket";
import {WebsocketServerEventHandler} from "./utils/websocket/websocketServerEventHandler"; import { WebsocketServerEventHandler } from "./utils/websocket/websocketServerEventHandler";
import {WebsocketEventHandler} from "./utils/websocket/websocketEventHandler"; import { WebsocketEventHandler } from "./utils/websocket/websocketEventHandler";
import {WebsocketEventType} from "./utils/websocket/websocketCustomEvents/websocketEventType"; import { WebsocketEventType } from "./utils/websocket/websocketCustomEvents/websocketEventType";
import { import {
appEventBus, appEventBus,
SPOTIFY_STATE_UPDATED_EVENT, SPOTIFY_STATE_UPDATED_EVENT,
USER_UPDATED_EVENT, USER_UPDATED_EVENT,
WEATHER_STATE_UPDATED_EVENT WEATHER_STATE_UPDATED_EVENT,
} from "./utils/eventBus"; } from "./utils/eventBus";
import {IUser} from "./db/models/user"; import { IUser } from "./db/models/user";
import {SpotifyPollingService} from "./services/spotifyPollingService"; import { SpotifyPollingService } from "./services/spotifyPollingService";
import {UserService} from "./services/db/UserService"; import { UserService } from "./services/db/UserService";
import {WeatherPollingService} from "./services/weatherPollingService"; import { WeatherPollingService } from "./services/weatherPollingService";
import {JwtAuthenticator} from "./utils/jwtAuthenticator"; import { JwtAuthenticator } from "./utils/jwtAuthenticator";
export class ExtendedWebSocketServer { export class ExtendedWebSocketServer {
private readonly _wss: WebSocketServer; private readonly _wss: WebSocketServer;
@@ -23,8 +23,13 @@ export class ExtendedWebSocketServer {
private readonly spotifyPollingService: SpotifyPollingService; private readonly spotifyPollingService: SpotifyPollingService;
private readonly weatherPollingService: WeatherPollingService; private readonly weatherPollingService: WeatherPollingService;
constructor(server: Server, userService: UserService, spotifyPollingService: SpotifyPollingService, constructor(
weatherPollingService: WeatherPollingService, jwtAuthenticator: JwtAuthenticator) { server: Server,
userService: UserService,
spotifyPollingService: SpotifyPollingService,
weatherPollingService: WeatherPollingService,
jwtAuthenticator: JwtAuthenticator
) {
this.userService = userService; this.userService = userService;
this.spotifyPollingService = spotifyPollingService; this.spotifyPollingService = spotifyPollingService;
this.weatherPollingService = weatherPollingService; this.weatherPollingService = weatherPollingService;
@@ -41,7 +46,7 @@ export class ExtendedWebSocketServer {
public broadcast(message: string): void { public broadcast(message: string): void {
this.getConnectedClients().forEach((client) => { this.getConnectedClients().forEach((client) => {
if (client.readyState === WebSocket.OPEN) { 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 { public sendMessageToUser(uuid: string, message: string): void {
const client = this._findClientByUUID(uuid); const client = this._findClientByUUID(uuid);
if (client && client.readyState === WebSocket.OPEN) { 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 { private _onNewClientReady(ws: ExtendedWebSocket): void {
console.log("WebSocket client connected and authenticated"); 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.enableErrorEvent();
socketEventHandler.enablePongEvent(); 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); const client = this._findClientByUUID(uuid);
console.log(`Received update for user ${uuid}`); console.log(`Received update for user ${uuid}`);
if (client) { if (client) {
client.send(JSON.stringify({ client.send(
type: "SPOTIFY_UPDATE", JSON.stringify({
payload: state, type: "SPOTIFY_UPDATE",
}), {binary: false}); 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) { for (const uuid of subscribers) {
const client = this._findClientByUUID(uuid); const client = this._findClientByUUID(uuid);
if (client) { if (client) {
client.send(JSON.stringify({ client.send(
type: "WEATHER_UPDATE", JSON.stringify({
payload: weatherData, type: "WEATHER_UPDATE",
}), {binary: false}); payload: weatherData,
}),
{ binary: false }
);
} }
} }
}); });
} }
private _findClientByUUID(uuid: string): ExtendedWebSocket | undefined { private _findClientByUUID(uuid: string): ExtendedWebSocket | undefined {
@@ -131,4 +145,4 @@ export class ExtendedWebSocketServer {
} }
return undefined; return undefined;
} }
} }