diff --git a/docker-compose.network.yml b/docker-compose.network.yml index 8cb41ad..7e2a7b1 100644 --- a/docker-compose.network.yml +++ b/docker-compose.network.yml @@ -87,7 +87,7 @@ services: environment: KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: zookeeper-jbx:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-jbx:9092,PLAINTEXT_HOST://localhost:29092 # KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT diff --git a/packages/config/constants.ts b/packages/config/constants.ts index e13eeae..7f342e8 100644 --- a/packages/config/constants.ts +++ b/packages/config/constants.ts @@ -9,3 +9,5 @@ export const LOG_NS = process.env.LOG_NS || 'server' export const KAFKA_BROKERS = process.env.KAFKA_BROKERS?.split(',') ?? ['kafka:9092'] export const KAFKA_GROUP_ID = process.env.KAFKA_GROUP_ID ?? 'jbx-server' +export const KAFKA_CONNECT_TIMEOUT_MS = +(process.env.KAFKA_CONNECT_TIMEOUT_MS ?? 60000) +export const KAFKA_REQ_TIMEOUT_MS = +(process.env.KAFKA_REQ_TIMEOUT_MS ?? KAFKA_CONNECT_TIMEOUT_MS) \ No newline at end of file diff --git a/packages/lib/kafka.ts b/packages/lib/kafka.ts index 4ea7c8c..3276bc8 100644 --- a/packages/lib/kafka.ts +++ b/packages/lib/kafka.ts @@ -6,7 +6,7 @@ */ import { Kafka, Partitioners, logLevel, type Message } from 'kafkajs' -import { KAFKA_BROKERS, KAFKA_GROUP_ID, NODE_ENV } from '@jukebox/config' +import { KAFKA_BROKERS, KAFKA_CONNECT_TIMEOUT_MS, KAFKA_GROUP_ID, KAFKA_REQ_TIMEOUT_MS, NODE_ENV } from '@jukebox/config' import { logger } from './logger' const toWinstonLogLevel = (level: logLevel) => { @@ -50,13 +50,13 @@ const getKafkaInstance = () => { brokers: KAFKA_BROKERS, logLevel: logLevel.INFO, logCreator: WinstonLogCreator, - connectionTimeout: 20000, - requestTimeout: 20000, + connectionTimeout: KAFKA_CONNECT_TIMEOUT_MS, + requestTimeout: KAFKA_REQ_TIMEOUT_MS, retry: { retries: 5, restartOnFailure: async () => true, - maxRetryTime: 20000 + maxRetryTime: KAFKA_REQ_TIMEOUT_MS } }) } else { diff --git a/server/docs/swagger.ts b/server/docs/swagger.ts index deb8adc..1d92a73 100644 --- a/server/docs/swagger.ts +++ b/server/docs/swagger.ts @@ -45,13 +45,22 @@ const doc = { definitions: { IGroupFields: { name: '', ownerId: '' } as IGroupFields, IGroup: { id: '', name: '', ownerId: '' } as IGroup, - IUser: new class implements IUser { + IUser: new (class implements IUser { id: string = 'some-id' email: string = 'user@example.com' firstName?: string | undefined lastName?: string | undefined image?: string | undefined - }() + })(), + IUserDetails: { + id: 'abc123', + firstName: 'John', + email: 'john@example.com', + lastName: 'Doe', + groups: [{ id: '456def', name: 'Example Group', ownerId: 'abc123' }], + image: + 'https://static.vecteezy.com/system/resources/thumbnails/001/840/618/small_2x/picture-profile-icon-male-icon-human-or-people-sign-and-symbol-free-vector.jpg' + } as IUser & { groups: IGroup[] } } } const generateResponseDocs = () => { diff --git a/server/docs/swagger_output.json b/server/docs/swagger_output.json index 9bcefb3..bfbddf6 100644 --- a/server/docs/swagger_output.json +++ b/server/docs/swagger_output.json @@ -5,7 +5,7 @@ "title": "Jukebox API", "description": "Documentation automatically generated by the swagger-autogen module." }, - "host": "localhost:8000", + "host": "localhost:8080", "basePath": "/", "tags": [ { @@ -534,24 +534,65 @@ "description": "", "responses": { "200": { + "schema": { + "$ref": "#/definitions/IUserDetails" + }, + "description": "OK" + }, + "400": { + "schema": { + "$ref": "#/definitions/Error400" + }, + "description": "Bad request" + }, + "404": { + "schema": { + "$ref": "#/definitions/Error404" + }, + "description": "Not found" + }, + "500": { + "schema": { + "$ref": "#/definitions/Error500" + }, + "description": "Internal Server Error" + }, + "501": { + "schema": { + "$ref": "#/definitions/Error501" + }, + "description": "Not implemented" + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "put": { + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", "schema": { "type": "object", "properties": { - "id": { - "type": "string", - "example": "66e9f875b14c1ccc11b3d8f0" + "firstName": { + "example": "any" }, - "email": { - "type": "string", - "example": "user@example.com" + "lastName": { + "example": "any" + }, + "image": { + "example": "any" } - }, - "xml": { - "name": "main" } - }, - "description": "OK" - }, + } + } + ], + "responses": { "400": { "schema": { "$ref": "#/definitions/Error400" @@ -1423,12 +1464,7 @@ }, "description": "Not implemented" } - }, - "security": [ - { - "Bearer": [] - } - ] + } } }, "/api/group/groups/{id}": { @@ -1658,6 +1694,51 @@ } } }, + "IUserDetails": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "abc123" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "email": { + "type": "string", + "example": "john@example.com" + }, + "lastName": { + "type": "string", + "example": "Doe" + }, + "groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "456def" + }, + "name": { + "type": "string", + "example": "Example Group" + }, + "ownerId": { + "type": "string", + "example": "abc123" + } + } + } + }, + "image": { + "type": "string", + "example": "https://static.vecteezy.com/system/resources/thumbnails/001/840/618/small_2x/picture-profile-icon-male-icon-human-or-people-sign-and-symbol-free-vector.jpg" + } + } + }, "Success200": { "type": "object", "properties": { diff --git a/server/models/groupModel.ts b/server/models/groupModel.ts index 01c0e9b..2c7f1dd 100644 --- a/server/models/groupModel.ts +++ b/server/models/groupModel.ts @@ -33,7 +33,8 @@ const GroupSchema = new mongoose.Schema( type: Types.ObjectId, ref: 'SpotifyAuth', unique: true, - dropDups: true + dropDups: true, + sparse: true }, defaultDeviceId: { type: String diff --git a/server/routes/groupRoutes.ts b/server/routes/groupRoutes.ts index 3a74dd4..61f0c39 100644 --- a/server/routes/groupRoutes.ts +++ b/server/routes/groupRoutes.ts @@ -12,7 +12,7 @@ router.get('/:id/spotify/auth', isAuthenticated, views.getGroupSpotifyAuthView) router.post('/:id/spotify/auth', isAuthenticated, views.assignSpotifyAccountView) router.post('/groups', isAuthenticated, views.groupCreateView) -router.get('/groups', isAuthenticated, views.groupListView) +router.get('/groups', views.groupListView) router.get('/groups/:id', isAuthenticated, views.groupGetView) router.put('/groups/:id', isAuthenticated, views.groupUpdateView) router.patch('/groups/:id', isAuthenticated, views.groupPartialUpdateView) diff --git a/server/routes/userRoutes.ts b/server/routes/userRoutes.ts index 05ceae6..ad863ec 100644 --- a/server/routes/userRoutes.ts +++ b/server/routes/userRoutes.ts @@ -11,6 +11,7 @@ router.post('/request-password-reset', isAuthenticated, views.requestPasswordRes router.post('/reset-password', isAuthenticated, views.resetPasswordView) router.get('/me', isAuthenticated, views.currentUserView) +router.put('/me', isAuthenticated, views.updateCurrentUserView) router.get('/me/spotify-accounts', isAuthenticated, views.connectedSpotifyAccounts) /**== User Management ==**/ diff --git a/server/views/userViews.ts b/server/views/userViews.ts index 319ef69..e258732 100644 --- a/server/views/userViews.ts +++ b/server/views/userViews.ts @@ -67,15 +67,23 @@ export const currentUserView = apiAuthRequest(async (req, res, next) => { /* #swagger.responses[200] = { - schema: { - id: "66e9f875b14c1ccc11b3d8f0", - email: "user@example.com" - }, + schema: { $ref: "#/definitions/IUserDetails" }, } */ return { ...userSerialized, groups } }) +export const updateCurrentUserView = apiAuthRequest(async (req, res, next) => { + const attrs: Partial = { + firstName: req.body.firstName, + lastName: req.body.lastName, + image: req.body.image + } + const { user } = res.locals + + return await User.findOneAndUpdate({ id: user._id }, attrs, { new: true }).exec() +}) + // TODO: Remove authentication requirement, send email to user export const requestPasswordResetView = apiRequest(async (req, res, next) => { /**