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