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) => {
/**