@ossy/users 1.13.0 → 1.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@ossy/users",
3
- "description": "User domain aggregate, events, and validators for the Ossy user model",
4
- "version": "1.13.0",
3
+ "description": "User domain - aggregate, events, and validators for the Ossy user model",
4
+ "version": "1.13.2",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "./src/index.js",
8
8
  "module": "./src/index.js",
9
9
  "exports": {
10
- ".": "./src/index.js"
10
+ ".": "./src/index.js",
11
+ "./server": "./src/server.js"
11
12
  },
12
13
  "scripts": {
13
14
  "test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose"
@@ -17,17 +18,22 @@
17
18
  "ossy": {
18
19
  "src": "./src"
19
20
  },
21
+ "peerDependencies": {
22
+ "@ossy/authentication": ">=1.0.0",
23
+ "@ossy/sdk-react": ">=1.0.0",
24
+ "react": ">=19.0.0 <20.0.0"
25
+ },
20
26
  "publishConfig": {
21
27
  "access": "public",
22
28
  "registry": "https://registry.npmjs.org"
23
29
  },
24
30
  "dependencies": {
25
- "@ossy/observability": "^1.8.0",
31
+ "@ossy/observability": "^1.8.2",
26
32
  "nanoid": "^5.1.11"
27
33
  },
28
34
  "devDependencies": {
29
35
  "@jest/globals": "^30.2.0",
30
- "@ossy/platform": "^1.39.0",
36
+ "@ossy/platform": "^1.39.2",
31
37
  "casual": "^1.6.2",
32
38
  "jest": "^30.2.0"
33
39
  },
@@ -35,5 +41,5 @@
35
41
  "/src",
36
42
  "README.md"
37
43
  ],
38
- "gitHead": "d9d31182be64a448d575da432af2830d909ad4b9"
44
+ "gitHead": "2b8745d57fee8b6c08787e2755b4df489291a5cd"
39
45
  }
@@ -1,35 +1 @@
1
- import jwt from 'jsonwebtoken'
2
- import { Aggregate } from '@ossy/event-store'
3
- import { Token, TokenEvents } from '@ossy/tokens'
4
- import { ConfigService } from '@ossy/platform'
5
- import { createLogger } from '@ossy/observability'
6
-
7
- const log = createLogger('users/create-api-token')
8
-
9
- const SECONDS_IN_100_YEARS = 60 * 60 * 24 * 365 * 100
10
-
11
- export const id = 'users/create-api-token'
12
- export const access = 'authenticated'
13
-
14
- export async function run({ payload, req }) {
15
- const createdBy = payload?.userId ?? req?.userId
16
- const name = payload?.name
17
- const description = payload?.description
18
-
19
- if (!name || typeof name !== 'string') {
20
- log.debug('[users/create-api-token] No name provided')
21
- throw Object.assign(new Error('No name provided'), { status: 400 })
22
- }
23
- if (!description || typeof description !== 'string') {
24
- log.debug('[users/create-api-token] No description provided')
25
- throw Object.assign(new Error('No description provided'), { status: 400 })
26
- }
27
-
28
- const expiresIn = SECONDS_IN_100_YEARS
29
- const expiresAt = Date.now() + expiresIn * 1000
30
- const token = jwt.sign({ sub: createdBy, type: 'Api' }, ConfigService.TokenSecret, { expiresIn })
31
-
32
- const event = TokenEvents.Created({ name, description, createdBy, type: 'Api', subject: createdBy, token, expiresAt })
33
-
34
- return Aggregate.Of(Token, event).then(Aggregate.View())
35
- }
1
+ export const metadata = { id: 'users/create-api-token', access: 'authenticated' }
@@ -0,0 +1,34 @@
1
+ import jwt from 'jsonwebtoken'
2
+ import { Aggregate } from '@ossy/event-store'
3
+ import { Token, TokenEvents } from '@ossy/tokens/server'
4
+ import { ConfigService } from '@ossy/platform'
5
+ import { createLogger } from '@ossy/observability'
6
+
7
+ export const metadata = { id: 'users/create-api-token' }
8
+
9
+ const log = createLogger('users/create-api-token')
10
+
11
+ const SECONDS_IN_100_YEARS = 60 * 60 * 24 * 365 * 100
12
+
13
+ export async function run({ payload, req }) {
14
+ const createdBy = payload?.userId ?? req?.userId
15
+ const name = payload?.name
16
+ const description = payload?.description
17
+
18
+ if (!name || typeof name !== 'string') {
19
+ log.debug('[users/create-api-token] No name provided')
20
+ throw Object.assign(new Error('No name provided'), { status: 400 })
21
+ }
22
+ if (!description || typeof description !== 'string') {
23
+ log.debug('[users/create-api-token] No description provided')
24
+ throw Object.assign(new Error('No description provided'), { status: 400 })
25
+ }
26
+
27
+ const expiresIn = SECONDS_IN_100_YEARS
28
+ const expiresAt = Date.now() + expiresIn * 1000
29
+ const token = jwt.sign({ sub: createdBy, type: 'Api' }, ConfigService.TokenSecret, { expiresIn })
30
+
31
+ const event = TokenEvents.Created({ name, description, createdBy, type: 'Api', subject: createdBy, token, expiresAt })
32
+
33
+ return Aggregate.Of(Token, event).then(Aggregate.View())
34
+ }
@@ -1,20 +1 @@
1
- import { Aggregate } from '@ossy/event-store'
2
- import { createLogger } from '@ossy/observability'
3
-
4
- const log = createLogger('users/get-api-tokens')
5
-
6
- export const id = 'users/get-api-tokens'
7
- export const access = 'authenticated'
8
-
9
- export async function run({ payload, req }) {
10
- const userId = payload?.userId ?? req?.userId
11
-
12
- log.info('[users/get-api-tokens] Fetching API tokens for user')
13
- const tokens = await Aggregate.Collection.find({
14
- type: 'Token',
15
- 'state.subject': userId,
16
- 'state.type': 'Api',
17
- }, { state: true }).toArray().then(docs => docs.map(d => d.state))
18
-
19
- return tokens.map(({ token: _secret, ...meta }) => meta)
20
- }
1
+ export const metadata = { id: 'users/get-api-tokens', access: 'authenticated' }
@@ -0,0 +1,19 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { createLogger } from '@ossy/observability'
3
+
4
+ export const metadata = { id: 'users/get-api-tokens' }
5
+
6
+ const log = createLogger('users/get-api-tokens')
7
+
8
+ export async function run({ payload, req }) {
9
+ const userId = payload?.userId ?? req?.userId
10
+
11
+ log.info('[users/get-api-tokens] Fetching API tokens for user')
12
+ const tokens = await Aggregate.Collection.find({
13
+ type: 'Token',
14
+ 'state.subject': userId,
15
+ 'state.type': 'Api',
16
+ }, { state: true }).toArray().then(docs => docs.map(d => d.state))
17
+
18
+ return tokens.map(({ token: _secret, ...meta }) => meta)
19
+ }
@@ -1,11 +1 @@
1
- import { EventStore } from '@ossy/event-store'
2
- import { User } from '@ossy/users'
3
-
4
- export const id = 'users/get-current-user-history'
5
- export const access = 'authenticated'
6
-
7
- export async function run({ payload, req }) {
8
- const userId = payload?.userId ?? req?.userId
9
- const events = await EventStore.GetEventStream({ aggregateId: userId, fromVersion: 0 })
10
- return User.History(events)
11
- }
1
+ export const metadata = { id: 'users/get-current-user-history', access: 'authenticated' }
@@ -0,0 +1,10 @@
1
+ import { EventStore } from '@ossy/event-store'
2
+ import { User } from '@ossy/users/server'
3
+
4
+ export const metadata = { id: 'users/get-current-user-history' }
5
+
6
+ export async function run({ payload, req }) {
7
+ const userId = payload?.userId ?? req?.userId
8
+ const events = await EventStore.GetEventStream({ aggregateId: userId, fromVersion: 0 })
9
+ return User.History(events)
10
+ }
package/src/index.js CHANGED
@@ -1,4 +1,7 @@
1
- export * from './user.aggregate.js'
2
- export * from './users.events.js'
3
- export * from './users.validators.js'
4
- export * from './users.queries.js'
1
+ export { metadata as GetUserApiTokens } from './get-api-tokens.action.js'
2
+ export { metadata as CreateUserApiToken } from './create-api-token.action.js'
3
+ export { metadata as InvalidateApiToken } from './invalidate-api-token.action.js'
4
+ export { metadata as UpdateUserDetails } from './update-details.action.js'
5
+ export { metadata as GetCurrentUserHistory } from './get-current-user-history.action.js'
6
+ export { metadata as JoinWorkspace } from './join-workspace.action.js'
7
+ export { metadata as LeaveWorkspace } from './leave-workspace.action.js'
@@ -1,16 +1 @@
1
- import { Aggregate } from '@ossy/event-store'
2
- import { Token, TokenEvents } from '@ossy/tokens'
3
-
4
- export const id = 'users/invalidate-api-token'
5
- export const access = 'authenticated'
6
-
7
- export async function run({ payload, req }) {
8
- const createdBy = payload?.userId ?? req?.userId
9
- const tokenId = payload?.tokenId ?? req?.params?.tokenId
10
-
11
- if (!tokenId) throw Object.assign(new Error('tokenId is required'), { status: 400 })
12
-
13
- const event = TokenEvents.Revoked({ createdBy })
14
- await Aggregate.Of(Token, tokenId).then(Aggregate.Add(event))
15
- return ''
16
- }
1
+ export const metadata = { id: 'users/invalidate-api-token', access: 'authenticated' }
@@ -0,0 +1,15 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { Token, TokenEvents } from '@ossy/tokens/server'
3
+
4
+ export const metadata = { id: 'users/invalidate-api-token' }
5
+
6
+ export async function run({ payload, req }) {
7
+ const createdBy = payload?.userId ?? req?.userId
8
+ const tokenId = payload?.tokenId ?? req?.params?.tokenId
9
+
10
+ if (!tokenId) throw Object.assign(new Error('tokenId is required'), { status: 400 })
11
+
12
+ const event = TokenEvents.Revoked({ createdBy })
13
+ await Aggregate.Of(Token, tokenId).then(Aggregate.Add(event))
14
+ return ''
15
+ }
@@ -1,18 +1 @@
1
- import { Aggregate } from '@ossy/event-store'
2
- import { User, UsersEvents } from '@ossy/users'
3
-
4
- export const id = 'users/join-workspace'
5
- export const access = 'authenticated'
6
-
7
- export async function run({ payload, req }) {
8
- const userId = payload?.targetUserId ?? payload?.userId ?? req?.userId
9
- const workspaceId = payload?.workspaceId
10
- const createdBy = payload?.createdBy ?? userId
11
-
12
- if (!workspaceId) throw Object.assign(new Error('workspaceId is required'), { status: 400 })
13
-
14
- await Aggregate.Of(User, userId)
15
- .then(Aggregate.Add(UsersEvents.WorkspaceJoined({ workspaceId, createdBy })))
16
- .then(Aggregate.Save())
17
- return null
18
- }
1
+ export const metadata = { id: 'users/join-workspace', access: 'authenticated' }
@@ -0,0 +1,17 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { User, UsersEvents } from '@ossy/users/server'
3
+
4
+ export const metadata = { id: 'users/join-workspace' }
5
+
6
+ export async function run({ payload, req }) {
7
+ const userId = payload?.targetUserId ?? payload?.userId ?? req?.userId
8
+ const workspaceId = payload?.workspaceId
9
+ const createdBy = payload?.createdBy ?? userId
10
+
11
+ if (!workspaceId) throw Object.assign(new Error('workspaceId is required'), { status: 400 })
12
+
13
+ await Aggregate.Of(User, userId)
14
+ .then(Aggregate.Add(UsersEvents.WorkspaceJoined({ workspaceId, createdBy })))
15
+ .then(Aggregate.Save())
16
+ return null
17
+ }
@@ -1,17 +1 @@
1
- import { Aggregate } from '@ossy/event-store'
2
- import { User, UsersEvents } from '@ossy/users'
3
-
4
- export const id = 'users/leave-workspace'
5
- export const access = 'authenticated'
6
-
7
- export async function run({ payload, req }) {
8
- const userId = payload?.userId ?? req?.userId
9
- const workspaceId = payload?.workspaceId
10
-
11
- if (!workspaceId) throw Object.assign(new Error('workspaceId is required'), { status: 400 })
12
-
13
- await Aggregate.Of(User, userId)
14
- .then(Aggregate.Add(UsersEvents.WorkspaceLeft({ workspaceId, createdBy: userId })))
15
- .then(Aggregate.Save())
16
- return null
17
- }
1
+ export const metadata = { id: 'users/leave-workspace', access: 'authenticated' }
@@ -0,0 +1,16 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { User, UsersEvents } from '@ossy/users/server'
3
+
4
+ export const metadata = { id: 'users/leave-workspace' }
5
+
6
+ export async function run({ payload, req }) {
7
+ const userId = payload?.userId ?? req?.userId
8
+ const workspaceId = payload?.workspaceId
9
+
10
+ if (!workspaceId) throw Object.assign(new Error('workspaceId is required'), { status: 400 })
11
+
12
+ await Aggregate.Of(User, userId)
13
+ .then(Aggregate.Add(UsersEvents.WorkspaceLeft({ workspaceId, createdBy: userId })))
14
+ .then(Aggregate.Save())
15
+ return null
16
+ }
package/src/server.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './user.aggregate.js'
2
+ export * from './users.events.js'
3
+ export * from './users.validators.js'
4
+ export * from './users.queries.js'
@@ -1,29 +1 @@
1
- import { Aggregate } from '@ossy/event-store'
2
- import { User, UsersEvents } from '@ossy/users'
3
-
4
- export const id = 'users/update-details'
5
- export const access = 'authenticated'
6
-
7
- export async function run({ payload, req }) {
8
- const createdBy = payload?.userId ?? req?.userId
9
- const currentUser = payload?.currentUser ?? req?.user
10
-
11
- const firstName = (payload?.firstName ?? currentUser?.firstName)?.trim?.()
12
- const lastName = (payload?.lastName ?? currentUser?.lastName)?.trim?.()
13
-
14
- if (!firstName || typeof firstName !== 'string') {
15
- throw Object.assign(new Error('No firstName provided'), { status: 400 })
16
- }
17
- if (!lastName || typeof lastName !== 'string') {
18
- throw Object.assign(new Error('No lastName provided'), { status: 400 })
19
- }
20
-
21
- const hasChanges = firstName + lastName !== currentUser?.firstName + currentUser?.lastName
22
- if (!hasChanges) {
23
- throw Object.assign(new Error('No changes provided'), { status: 400 })
24
- }
25
-
26
- return Aggregate.Of(User, createdBy)
27
- .then(Aggregate.Add(UsersEvents.NameUpdated({ firstName, lastName, createdBy })))
28
- .then(Aggregate.View())
29
- }
1
+ export const metadata = { id: 'users/update-details', access: 'authenticated' }
@@ -0,0 +1,28 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { User, UsersEvents } from '@ossy/users/server'
3
+
4
+ export const metadata = { id: 'users/update-details' }
5
+
6
+ export async function run({ payload, req }) {
7
+ const createdBy = payload?.userId ?? req?.userId
8
+ const currentUser = payload?.currentUser ?? req?.user
9
+
10
+ const firstName = (payload?.firstName ?? currentUser?.firstName)?.trim?.()
11
+ const lastName = (payload?.lastName ?? currentUser?.lastName)?.trim?.()
12
+
13
+ if (!firstName || typeof firstName !== 'string') {
14
+ throw Object.assign(new Error('No firstName provided'), { status: 400 })
15
+ }
16
+ if (!lastName || typeof lastName !== 'string') {
17
+ throw Object.assign(new Error('No lastName provided'), { status: 400 })
18
+ }
19
+
20
+ const hasChanges = firstName + lastName !== currentUser?.firstName + currentUser?.lastName
21
+ if (!hasChanges) {
22
+ throw Object.assign(new Error('No changes provided'), { status: 400 })
23
+ }
24
+
25
+ return Aggregate.Of(User, createdBy)
26
+ .then(Aggregate.Add(UsersEvents.NameUpdated({ firstName, lastName, createdBy })))
27
+ .then(Aggregate.View())
28
+ }
package/src/users.spec.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import casual from 'casual'
2
2
  import { TestUtil, getApiTestBaseUrl, getSetCookieHeader } from '@ossy/platform/test'
3
3
 
4
- const isUUID = x =>
5
- /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
6
- .test(x)
4
+ const authHeaders = (token) => ({
5
+ 'Content-Type': 'application/json',
6
+ Authorization: token,
7
+ })
7
8
 
8
9
  const invalidEmails = [
9
10
  undefined,
@@ -22,36 +23,32 @@ const invalidVerifySignInCases = [
22
23
  { label: 'numeric string', query: 'token=2', status: 401, body: '' },
23
24
  ]
24
25
 
25
- function invalidSignUpBody(invalidEmail) {
26
+ function invalidSignUpPayload(invalidEmail) {
26
27
  const o = { firstName: 'Test', lastName: 'User' }
27
28
  if (invalidEmail !== undefined) o.email = invalidEmail
28
- return JSON.stringify(o)
29
+ return o
29
30
  }
30
31
 
31
- describe('[/users/sign-up][POST]', () => {
32
+ describe('[authentication/sign-up]', () => {
32
33
 
33
34
  describe('given an email that is not already in use', () => {
34
35
 
35
- it('must return OK 200', () => TestUtil.AssertResponse({
36
- endpoint: '/users/sign-up',
37
- method: 'POST',
38
- headers: { 'Content-Type': 'application/json'},
39
- body: TestUtil.signUpBody({ email: casual.email }),
40
- expectedResponseStatus: 200,
41
- expectedResponseBody: ''
36
+ it('must return OK 200', () => TestUtil.AssertActionResponse({
37
+ actionId: 'authentication/sign-up',
38
+ body: TestUtil.signUpBody({ email: casual.email }),
39
+ expectedResponseStatus: 200,
40
+ expectedResponseBody: { ok: true }
42
41
  }))
43
42
 
44
43
  it('must produce a SignedUp event', async () => {
45
44
 
46
45
  const email = casual.email
47
46
 
48
- await TestUtil.AssertResponse({
49
- endpoint: '/users/sign-up',
50
- method: 'POST',
51
- headers: { 'Content-Type': 'application/json'},
52
- body: TestUtil.signUpBody({ email }),
53
- expectedResponseStatus: 200,
54
- expectedResponseBody: ''
47
+ await TestUtil.AssertActionResponse({
48
+ actionId: 'authentication/sign-up',
49
+ body: TestUtil.signUpBody({ email }),
50
+ expectedResponseStatus: 200,
51
+ expectedResponseBody: { ok: true }
55
52
  })
56
53
 
57
54
  await TestUtil.AssertEventExist({
@@ -70,32 +67,28 @@ describe('[/users/sign-up][POST]', () => {
70
67
  const email = casual.email
71
68
 
72
69
  const signUpTestRequest = {
73
- endpoint: '/users/sign-up',
74
- method: 'POST',
75
- headers: { 'Content-Type': 'application/json'},
76
- body: TestUtil.signUpBody({ email }),
77
- expectedResponseStatus: 200,
78
- expectedResponseBody: ''
70
+ actionId: 'authentication/sign-up',
71
+ body: TestUtil.signUpBody({ email }),
72
+ expectedResponseStatus: 200,
73
+ expectedResponseBody: { ok: true }
79
74
  }
80
75
 
81
- await TestUtil.AssertResponse(signUpTestRequest)
82
- await TestUtil.AssertResponse(signUpTestRequest)
76
+ await TestUtil.AssertActionResponse(signUpTestRequest)
77
+ await TestUtil.AssertActionResponse(signUpTestRequest)
83
78
  })
84
79
 
85
80
  it('must not produce another SignUp event', async () => {
86
81
  const email = `dup-${Date.now()}-${casual.email}`
87
82
 
88
83
  const signUpTestRequest = {
89
- endpoint: '/users/sign-up',
90
- method: 'POST',
91
- headers: { 'Content-Type': 'application/json'},
92
- body: TestUtil.signUpBody({ email }),
93
- expectedResponseStatus: 200,
94
- expectedResponseBody: ''
84
+ actionId: 'authentication/sign-up',
85
+ body: TestUtil.signUpBody({ email }),
86
+ expectedResponseStatus: 200,
87
+ expectedResponseBody: { ok: true }
95
88
  }
96
89
 
97
- await TestUtil.AssertResponse(signUpTestRequest)
98
- await TestUtil.AssertResponse(signUpTestRequest)
90
+ await TestUtil.AssertActionResponse(signUpTestRequest)
91
+ await TestUtil.AssertActionResponse(signUpTestRequest)
99
92
 
100
93
  const signUpEvents = await TestUtil.GetEvents({
101
94
  aggregateType: 'User',
@@ -113,13 +106,11 @@ describe('[/users/sign-up][POST]', () => {
113
106
  invalidEmails.forEach(invalidEmail => {
114
107
  it(
115
108
  `must return BAD REQUEST 400 for invalid email: ${invalidEmail}`,
116
- () => TestUtil.AssertResponse({
117
- endpoint: '/users/sign-up',
118
- method: 'POST',
119
- headers: { 'Content-Type': 'application/json'},
120
- body: invalidSignUpBody(invalidEmail),
109
+ () => TestUtil.AssertActionResponse({
110
+ actionId: 'authentication/sign-up',
111
+ payload: invalidSignUpPayload(invalidEmail),
121
112
  expectedResponseStatus: 400,
122
- expectedResponseBody: 'Invalid email'
113
+ expectedResponseBody: { error: 'Invalid email' }
123
114
  })
124
115
  )
125
116
  })
@@ -127,31 +118,25 @@ describe('[/users/sign-up][POST]', () => {
127
118
 
128
119
  })
129
120
 
130
- describe('[/users/sign-in][POST]', () => {
121
+ describe('[authentication/request-sign-in]', () => {
131
122
 
132
123
  describe('given an email that is in use', () => {
133
124
 
134
125
  it('must return an OK 200 response', async () => {
135
126
  const email = casual.email
136
127
 
137
- await TestUtil.AssertResponse({
138
- endpoint: '/users/sign-up',
139
- description: '',
140
- method: 'POST',
141
- headers: { 'Content-Type': 'application/json'},
142
- body: TestUtil.signUpBody({ email }),
143
- expectedResponseStatus: 200,
144
- expectedResponseBody: ''
128
+ await TestUtil.AssertActionResponse({
129
+ actionId: 'authentication/sign-up',
130
+ body: TestUtil.signUpBody({ email }),
131
+ expectedResponseStatus: 200,
132
+ expectedResponseBody: { ok: true }
145
133
  })
146
134
 
147
- await TestUtil.AssertResponse({
148
- endpoint: '/users/sign-in',
149
- description: '',
150
- method: 'POST',
151
- headers: { 'Content-Type': 'application/json'},
152
- body: JSON.stringify({ email }),
153
- expectedResponseStatus: 200,
154
- expectedResponseBody: ''
135
+ await TestUtil.AssertActionResponse({
136
+ actionId: 'authentication/request-sign-in',
137
+ payload: { email },
138
+ expectedResponseStatus: 200,
139
+ expectedResponseBody: { ok: true }
155
140
  })
156
141
 
157
142
  })
@@ -160,24 +145,18 @@ describe('[/users/sign-in][POST]', () => {
160
145
 
161
146
  const email = casual.email
162
147
 
163
- await TestUtil.AssertResponse({
164
- endpoint: '/users/sign-up',
165
- description: '',
166
- method: 'POST',
167
- headers: { 'Content-Type': 'application/json'},
168
- body: TestUtil.signUpBody({ email }),
169
- expectedResponseStatus: 200,
170
- expectedResponseBody: ''
148
+ await TestUtil.AssertActionResponse({
149
+ actionId: 'authentication/sign-up',
150
+ body: TestUtil.signUpBody({ email }),
151
+ expectedResponseStatus: 200,
152
+ expectedResponseBody: { ok: true }
171
153
  })
172
154
 
173
- await TestUtil.AssertResponse({
174
- endpoint: '/users/sign-in',
175
- description: '',
176
- method: 'POST',
177
- headers: { 'Content-Type': 'application/json'},
178
- body: JSON.stringify({ email }),
179
- expectedResponseStatus: 200,
180
- expectedResponseBody: ''
155
+ await TestUtil.AssertActionResponse({
156
+ actionId: 'authentication/request-sign-in',
157
+ payload: { email },
158
+ expectedResponseStatus: 200,
159
+ expectedResponseBody: { ok: true }
181
160
  })
182
161
 
183
162
  const signedUpEvent = await TestUtil.GetEvent({
@@ -195,14 +174,11 @@ describe('[/users/sign-in][POST]', () => {
195
174
  describe('given an email that is not in use', () => {
196
175
 
197
176
  it('must, for security reasons, return an OK 200 response', async () => {
198
- await TestUtil.AssertResponse({
199
- endpoint: '/users/sign-in',
200
- description: '',
201
- method: 'POST',
202
- headers: { 'Content-Type': 'application/json'},
203
- body: JSON.stringify({ email: casual.email }),
204
- expectedResponseStatus: 200,
205
- expectedResponseBody: ''
177
+ await TestUtil.AssertActionResponse({
178
+ actionId: 'authentication/request-sign-in',
179
+ payload: { email: casual.email },
180
+ expectedResponseStatus: 200,
181
+ expectedResponseBody: { ok: true }
206
182
  })
207
183
  })
208
184
 
@@ -210,14 +186,11 @@ describe('[/users/sign-in][POST]', () => {
210
186
 
211
187
  const verificationTokensBefore = await TestUtil.countVerificationTokenEvents()
212
188
 
213
- await TestUtil.AssertResponse({
214
- endpoint: '/users/sign-in',
215
- description: '',
216
- method: 'POST',
217
- headers: { 'Content-Type': 'application/json'},
218
- body: JSON.stringify({ email: casual.email }),
219
- expectedResponseStatus: 200,
220
- expectedResponseBody: ''
189
+ await TestUtil.AssertActionResponse({
190
+ actionId: 'authentication/request-sign-in',
191
+ payload: { email: casual.email },
192
+ expectedResponseStatus: 200,
193
+ expectedResponseBody: { ok: true }
221
194
  })
222
195
 
223
196
  const verificationTokensAfter = await TestUtil.countVerificationTokenEvents()
@@ -233,13 +206,11 @@ describe('[/users/sign-in][POST]', () => {
233
206
  invalidEmails.forEach(invalidEmail => {
234
207
  it(
235
208
  `must return BAD REQUEST 400 for invalid email: ${invalidEmail}`,
236
- () => TestUtil.AssertResponse({
237
- endpoint: '/users/sign-in',
238
- method: 'POST',
239
- headers: { 'Content-Type': 'application/json'},
240
- body: JSON.stringify({ email: invalidEmail }),
209
+ () => TestUtil.AssertActionResponse({
210
+ actionId: 'authentication/request-sign-in',
211
+ payload: { email: invalidEmail },
241
212
  expectedResponseStatus: 400,
242
- expectedResponseBody: { message: 'No email provided' }
213
+ expectedResponseBody: { error: 'No email provided' }
243
214
  })
244
215
  )
245
216
  })
@@ -253,13 +224,11 @@ describe('[/users/verify-sign-in][GET]', () => {
253
224
  it('must return OK 200 and an auth cookie and must produce a SignInVerified event', async () => {
254
225
  const email = casual.email
255
226
 
256
- await TestUtil.AssertResponse({
257
- endpoint: '/users/sign-up',
258
- method: 'POST',
259
- headers: { 'Content-Type': 'application/json'},
260
- body: TestUtil.signUpBody({ email }),
261
- expectedResponseStatus: 200,
262
- expectedResponseBody: ''
227
+ await TestUtil.AssertActionResponse({
228
+ actionId: 'authentication/sign-up',
229
+ body: TestUtil.signUpBody({ email }),
230
+ expectedResponseStatus: 200,
231
+ expectedResponseBody: { ok: true }
263
232
  })
264
233
 
265
234
  const signedUpEvent = await TestUtil.GetEvent({
@@ -294,22 +263,18 @@ describe('[/users/verify-sign-in][GET]', () => {
294
263
  it('must return OK 200 and an auth cookie and must produce a SignInVerified event', async () => {
295
264
  const email = casual.email
296
265
 
297
- await TestUtil.AssertResponse({
298
- endpoint: '/users/sign-up',
299
- method: 'POST',
300
- headers: { 'Content-Type': 'application/json'},
301
- body: TestUtil.signUpBody({ email }),
302
- expectedResponseStatus: 200,
303
- expectedResponseBody: ''
266
+ await TestUtil.AssertActionResponse({
267
+ actionId: 'authentication/sign-up',
268
+ body: TestUtil.signUpBody({ email }),
269
+ expectedResponseStatus: 200,
270
+ expectedResponseBody: { ok: true }
304
271
  })
305
272
 
306
- await TestUtil.AssertResponse({
307
- endpoint: '/users/sign-in',
308
- method: 'POST',
309
- headers: { 'Content-Type': 'application/json'},
310
- body: JSON.stringify({ email }),
311
- expectedResponseStatus: 200,
312
- expectedResponseBody: ''
273
+ await TestUtil.AssertActionResponse({
274
+ actionId: 'authentication/request-sign-in',
275
+ payload: { email },
276
+ expectedResponseStatus: 200,
277
+ expectedResponseBody: { ok: true }
313
278
  })
314
279
 
315
280
  const signedUpEvent = await TestUtil.GetEvent({
@@ -369,22 +334,23 @@ describe('[/users/verify-sign-in][GET]', () => {
369
334
 
370
335
  })
371
336
 
372
- describe('[/users/me][GET]', () => {
337
+ describe('[authentication/get-current-user]', () => {
373
338
 
374
- TestUtil.AssertAuthenticationNeeded({
375
- endpoint: '/users/me',
376
- method: 'GET',
377
- headers: { 'Content-Type': 'application/json' },
339
+ describe('given no auth token is provided', () => {
340
+ it('must return null', async () => {
341
+ const response = await TestUtil.InvokeAction({ actionId: 'authentication/get-current-user' })
342
+ expect(response.status).toEqual(200)
343
+ await expect(response.json()).resolves.toBeNull()
344
+ })
378
345
  })
379
346
 
380
347
  describe('given an auth token', () => {
381
348
  it('must return OK 200 with a view of the current user state', async () => {
382
349
  const user = await TestUtil.GetAuthenticatedTestUser()
383
350
 
384
- const response = await TestUtil.MakeRequest({
385
- endpoint: '/users/me',
386
- method: 'GET',
387
- headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
351
+ const response = await TestUtil.InvokeAction({
352
+ actionId: 'authentication/get-current-user',
353
+ headers: authHeaders(user.token),
388
354
  })
389
355
 
390
356
  const body = await response.json()
@@ -404,22 +370,19 @@ describe('[/users/me][GET]', () => {
404
370
  })
405
371
  })
406
372
 
407
- describe('[/users/me/history][GET]', () => {
373
+ describe('[users/get-current-user-history]', () => {
408
374
 
409
- TestUtil.AssertAuthenticationNeeded({
410
- endpoint: '/users/me/history',
411
- method: 'GET',
412
- headers: { 'Content-Type': 'application/json' },
375
+ TestUtil.AssertActionAuthenticationNeeded({
376
+ actionId: 'users/get-current-user-history',
413
377
  })
414
378
 
415
379
  describe('given an auth token', () => {
416
380
  it('must return OK 200 with a view of the history of the current user', async () => {
417
381
  const user = await TestUtil.GetAuthenticatedTestUser()
418
382
 
419
- const response = await TestUtil.MakeRequest({
420
- endpoint: '/users/me/history',
421
- method: 'GET',
422
- headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
383
+ const response = await TestUtil.InvokeAction({
384
+ actionId: 'users/get-current-user-history',
385
+ headers: authHeaders(user.token),
423
386
  })
424
387
 
425
388
  const body = await response.json()
@@ -436,24 +399,21 @@ describe('[/users/me/history][GET]', () => {
436
399
  })
437
400
  })
438
401
 
439
- describe('[/users/me/tokens][POST]', () => {
402
+ describe('[users/create-api-token]', () => {
440
403
 
441
- TestUtil.AssertAuthenticationNeeded({
442
- endpoint: '/users/me/tokens',
443
- method: 'POST',
444
- headers: { 'Content-Type': 'application/json' },
445
- body: JSON.stringify({ name: 't', description: 'd' })
404
+ TestUtil.AssertActionAuthenticationNeeded({
405
+ actionId: 'users/create-api-token',
406
+ payload: { name: 't', description: 'd' }
446
407
  })
447
408
 
448
409
  describe('given a valid auth token', () => {
449
410
  it('must return OK 200 with an API token and produce an ApiTokenCreated event', async () => {
450
411
  const user = await TestUtil.GetAuthenticatedTestUser()
451
412
 
452
- const response = await TestUtil.MakeRequest({
453
- endpoint: '/users/me/tokens',
454
- method: 'POST',
455
- headers: { 'Content-Type': 'application/json', 'Authorization': user.token },
456
- body: JSON.stringify({ name: 'cli', description: casual.sentence })
413
+ const response = await TestUtil.InvokeAction({
414
+ actionId: 'users/create-api-token',
415
+ headers: authHeaders(user.token),
416
+ payload: { name: 'cli', description: casual.sentence }
457
417
  })
458
418
 
459
419
  const responseBody = await response.json()
@@ -487,11 +447,10 @@ describe('[/users/me/tokens][POST]', () => {
487
447
  else if (invalidDescription === '') payload = { name: 'token', description: '' }
488
448
  else payload = invalidDescription
489
449
 
490
- const response = await TestUtil.MakeRequest({
491
- endpoint: '/users/me/tokens',
492
- method: 'POST',
493
- headers: { 'Content-Type': 'application/json', 'Authorization': user.token },
494
- body: JSON.stringify(payload)
450
+ const response = await TestUtil.InvokeAction({
451
+ actionId: 'users/create-api-token',
452
+ headers: authHeaders(user.token),
453
+ payload,
495
454
  })
496
455
 
497
456
  const responseBody = await response.json()
@@ -504,7 +463,7 @@ describe('[/users/me/tokens][POST]', () => {
504
463
 
505
464
  expect(response.status).toEqual(400)
506
465
  expect(response.headers.get('Content-Type').includes('application/json')).toEqual(true)
507
- expect(responseBody).toEqual('')
466
+ expect(responseBody).toEqual({ error: 'No description provided' })
508
467
  await expect(event).rejects.toEqual(undefined)
509
468
  })
510
469
  })
@@ -512,12 +471,10 @@ describe('[/users/me/tokens][POST]', () => {
512
471
 
513
472
  })
514
473
 
515
- describe('[/users/me/tokens][GET]', () => {
474
+ describe('[users/get-api-tokens]', () => {
516
475
 
517
- TestUtil.AssertAuthenticationNeeded({
518
- endpoint: '/users/me/tokens',
519
- method: 'GET',
520
- headers: { 'Content-Type': 'application/json' },
476
+ TestUtil.AssertActionAuthenticationNeeded({
477
+ actionId: 'users/get-api-tokens',
521
478
  })
522
479
 
523
480
  describe('given authentication token is provided', () => {
@@ -525,17 +482,15 @@ describe('[/users/me/tokens][GET]', () => {
525
482
 
526
483
  const user = await TestUtil.GetAuthenticatedTestUser()
527
484
 
528
- const token = await TestUtil.MakeRequest({
529
- endpoint: '/users/me/tokens',
530
- method: 'POST',
531
- headers: { 'Content-Type': 'application/json', 'Authorization': user.token },
532
- body: JSON.stringify({ name: 'list-test', description: casual.sentence })
485
+ const token = await TestUtil.InvokeAction({
486
+ actionId: 'users/create-api-token',
487
+ headers: authHeaders(user.token),
488
+ payload: { name: 'list-test', description: casual.sentence }
533
489
  }).then(response => response.json())
534
490
 
535
- const response = await TestUtil.MakeRequest({
536
- endpoint: '/users/me/tokens',
537
- method: 'GET',
538
- headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
491
+ const response = await TestUtil.InvokeAction({
492
+ actionId: 'users/get-api-tokens',
493
+ headers: authHeaders(user.token),
539
494
  })
540
495
 
541
496
  const responseBody = await response.json()
@@ -560,22 +515,20 @@ describe('[/users/me/tokens][GET]', () => {
560
515
  })
561
516
 
562
517
  const createApiToken = async (user) => {
563
- const token = await TestUtil.MakeRequest({
564
- endpoint: '/users/me/tokens',
565
- method: 'POST',
566
- headers: { 'Content-Type': 'application/json', 'Authorization': user.token },
567
- body: JSON.stringify({ name: 'delete-test', description: casual.sentence })
518
+ const token = await TestUtil.InvokeAction({
519
+ actionId: 'users/create-api-token',
520
+ headers: authHeaders(user.token),
521
+ payload: { name: 'delete-test', description: casual.sentence }
568
522
  }).then(response => response.json())
569
523
 
570
524
  return token
571
525
  }
572
526
 
573
- describe('[/users/me/tokens/:tokenId][DELETE]', () => {
527
+ describe('[users/invalidate-api-token]', () => {
574
528
 
575
- TestUtil.AssertAuthenticationNeeded({
576
- endpoint: `/users/me/tokens/123`,
577
- method: 'DELETE',
578
- headers: { 'Content-Type': 'application/json' },
529
+ TestUtil.AssertActionAuthenticationNeeded({
530
+ actionId: 'users/invalidate-api-token',
531
+ payload: { tokenId: '123' },
579
532
  })
580
533
 
581
534
  describe('authentication with user token', () => {
@@ -584,10 +537,10 @@ describe('[/users/me/tokens/:tokenId][DELETE]', () => {
584
537
  const user = await TestUtil.GetAuthenticatedTestUser()
585
538
  const token = await createApiToken(user)
586
539
 
587
- const response = await TestUtil.MakeRequest({
588
- endpoint: `/users/me/tokens/${token.id}`,
589
- method: 'DELETE',
590
- headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
540
+ const response = await TestUtil.InvokeAction({
541
+ actionId: 'users/invalidate-api-token',
542
+ headers: authHeaders(user.token),
543
+ payload: { tokenId: token.id },
591
544
  })
592
545
 
593
546
  const responseBody = await response.json()
@@ -608,16 +561,15 @@ describe('[/users/me/tokens/:tokenId][DELETE]', () => {
608
561
  const user = await TestUtil.GetAuthenticatedTestUser()
609
562
  const token = await createApiToken(user)
610
563
 
611
- await TestUtil.MakeRequest({
612
- endpoint: `/users/me/tokens/${token.id}`,
613
- method: 'DELETE',
614
- headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
564
+ await TestUtil.InvokeAction({
565
+ actionId: 'users/invalidate-api-token',
566
+ headers: authHeaders(user.token),
567
+ payload: { tokenId: token.id },
615
568
  })
616
569
 
617
- const response = await TestUtil.MakeRequest({
618
- endpoint: '/users/me/tokens',
619
- method: 'GET',
620
- headers: { 'Content-Type': 'application/json', 'Authorization': user.token }
570
+ const response = await TestUtil.InvokeAction({
571
+ actionId: 'users/get-api-tokens',
572
+ headers: authHeaders(user.token),
621
573
  })
622
574
 
623
575
  const responseBody = await response.json()
@@ -634,10 +586,10 @@ describe('[/users/me/tokens/:tokenId][DELETE]', () => {
634
586
  const user = await TestUtil.GetAuthenticatedTestUser()
635
587
  const token = await createApiToken(user)
636
588
 
637
- const response = await TestUtil.MakeRequest({
638
- endpoint: `/users/me/tokens/${token.id}`,
639
- method: 'DELETE',
640
- headers: { 'Content-Type': 'application/json', 'Authorization': token.token }
589
+ const response = await TestUtil.InvokeAction({
590
+ actionId: 'users/invalidate-api-token',
591
+ headers: authHeaders(token.token),
592
+ payload: { tokenId: token.id },
641
593
  })
642
594
 
643
595
  const responseBody = await response.json()
@@ -658,22 +610,21 @@ describe('[/users/me/tokens/:tokenId][DELETE]', () => {
658
610
  const user = await TestUtil.GetAuthenticatedTestUser()
659
611
  const token = await createApiToken(user)
660
612
 
661
- await TestUtil.MakeRequest({
662
- endpoint: `/users/me/tokens/${token.id}`,
663
- method: 'DELETE',
664
- headers: { 'Content-Type': 'application/json', 'Authorization': token.token }
613
+ await TestUtil.InvokeAction({
614
+ actionId: 'users/invalidate-api-token',
615
+ headers: authHeaders(token.token),
616
+ payload: { tokenId: token.id },
665
617
  })
666
618
 
667
- const response = await TestUtil.MakeRequest({
668
- endpoint: '/users/me/tokens',
669
- method: 'GET',
670
- headers: { 'Content-Type': 'application/json', 'Authorization': token.token }
619
+ const response = await TestUtil.InvokeAction({
620
+ actionId: 'users/get-api-tokens',
621
+ headers: authHeaders(token.token),
671
622
  })
672
623
 
673
624
  const responseBody = await response.json()
674
625
 
675
626
  expect(response.status).toEqual(401)
676
- expect(responseBody).toEqual('')
627
+ expect(responseBody).toEqual({ error: 'Unauthorized' })
677
628
  })
678
629
 
679
630
  })
@@ -1,5 +1,5 @@
1
1
  import { Aggregate } from '@ossy/event-store'
2
- import { UsersEvents, User } from './index.js'
2
+ import { UsersEvents, User } from './server.js'
3
3
  import { createLogger } from '@ossy/observability'
4
4
 
5
5
  const log = createLogger('users')
package/src/list.api.js DELETED
@@ -1,23 +0,0 @@
1
- import { ActionService } from '@ossy/platform'
2
-
3
- export const metadata = {
4
- id: 'users.list',
5
- path: '/api/v0/users',
6
- }
7
-
8
- export default async function handle(req, res) {
9
- if (req.method !== 'GET') {
10
- res.setHeader('Allow', 'GET')
11
- res.status(405).json({ error: 'Method Not Allowed' })
12
- return
13
- }
14
- try {
15
- const users = await ActionService.invoke('workspaces/get-users', {
16
- payload: { workspaceId: req.workspaceId },
17
- req,
18
- })
19
- res.json(users)
20
- } catch (err) {
21
- res.status(err?.status ?? 500).json()
22
- }
23
- }
@@ -1,23 +0,0 @@
1
- import { ActionService } from '@ossy/platform'
2
-
3
- export const metadata = {
4
- id: 'users.me.history',
5
- path: '/api/v0/users/me/history',
6
- }
7
-
8
- export default async function handle(req, res) {
9
- if (req.method !== 'GET') {
10
- res.setHeader('Allow', 'GET')
11
- res.status(405).json({ error: 'Method Not Allowed' })
12
- return
13
- }
14
- try {
15
- const history = await ActionService.invoke('users/get-current-user-history', {
16
- payload: { userId: req.userId },
17
- req,
18
- })
19
- res.status(200).json(history)
20
- } catch (err) {
21
- res.status(err?.status ?? 500).json('')
22
- }
23
- }
@@ -1,32 +0,0 @@
1
- import { ActionService } from '@ossy/platform'
2
-
3
- export const metadata = {
4
- id: 'users.me.token',
5
- path: '/api/v0/users/me/tokens/:tokenId',
6
- }
7
-
8
- export default async function handle(req, res) {
9
- if (req.method !== 'DELETE') {
10
- res.setHeader('Allow', 'DELETE')
11
- res.status(405).json({ error: 'Method Not Allowed' })
12
- return
13
- }
14
-
15
- const pathname = (req.path || new URL(req.originalUrl, 'http://localhost').pathname).replace(/\/+$/, '')
16
- const m = pathname.match(/^\/api\/v0\/users\/me\/tokens\/([^/]+)$/)
17
- if (!m) {
18
- res.status(404).json('')
19
- return
20
- }
21
-
22
- const tokenId = decodeURIComponent(m[1])
23
- try {
24
- await ActionService.invoke('users/invalidate-api-token', {
25
- payload: { userId: req.userId, tokenId },
26
- req,
27
- })
28
- res.status(200).json('')
29
- } catch (err) {
30
- res.status(err?.status ?? 500).json()
31
- }
32
- }
@@ -1,35 +0,0 @@
1
- import { ActionService } from '@ossy/platform'
2
-
3
- export const metadata = {
4
- id: 'users.me.tokens',
5
- path: '/api/v0/users/me/tokens',
6
- }
7
-
8
- export default async function handle(req, res) {
9
- if (req.method === 'GET') {
10
- try {
11
- const tokens = await ActionService.invoke('users/get-api-tokens', {
12
- payload: { userId: req.userId },
13
- req,
14
- })
15
- res.json(tokens)
16
- } catch (err) {
17
- res.status(err?.status ?? 500).json()
18
- }
19
- return
20
- }
21
- if (req.method === 'POST') {
22
- try {
23
- const token = await ActionService.invoke('users/create-api-token', {
24
- payload: { userId: req.userId, name: req.body?.name, description: req.body?.description },
25
- req,
26
- })
27
- res.json(token)
28
- } catch (err) {
29
- res.status(err?.status ?? 400).json('')
30
- }
31
- return
32
- }
33
- res.setHeader('Allow', 'GET, POST')
34
- res.status(405).json({ error: 'Method Not Allowed' })
35
- }
package/src/me.api.js DELETED
@@ -1,41 +0,0 @@
1
- import { ActionService } from '@ossy/platform'
2
-
3
- export const metadata = {
4
- id: 'users.me',
5
- path: '/api/v0/users/me',
6
- }
7
-
8
- export default async function handle(req, res) {
9
- if (req.method === 'GET' || req.method === 'POST') {
10
- try {
11
- if (!req.user || req.user.anonymous) {
12
- res.status(401).json('')
13
- return
14
- }
15
- const user = await ActionService.invoke('authentication/get-current-user', { req })
16
- res.status(200).json(user)
17
- } catch (err) {
18
- res.status(err?.status ?? 500).json('')
19
- }
20
- return
21
- }
22
- if (req.method === 'PUT') {
23
- try {
24
- const user = await ActionService.invoke('users/update-details', {
25
- payload: {
26
- userId: req.userId,
27
- firstName: req.body?.firstName,
28
- lastName: req.body?.lastName,
29
- currentUser: req.user,
30
- },
31
- req,
32
- })
33
- res.status(200).json(user)
34
- } catch (err) {
35
- res.status(err?.status ?? 400).json(err?.message)
36
- }
37
- return
38
- }
39
- res.setHeader('Allow', 'GET, POST, PUT')
40
- res.status(405).json({ error: 'Method Not Allowed' })
41
- }
@@ -1,72 +0,0 @@
1
- import { ConfigService } from '@ossy/platform'
2
- import { ActionService } from '@ossy/platform'
3
- import { createLogger } from '@ossy/observability'
4
-
5
- const log = createLogger('users')
6
-
7
- function isBotUser(req) {
8
- return req.userId === ConfigService.BotUserId
9
- }
10
-
11
- export const metadata = {
12
- id: 'users.workspace-membership',
13
- path: '/api/v0/users/:userId/workspaces',
14
- }
15
-
16
- export default async function handle(req, res) {
17
- const pathname = (req.path || new URL(req.originalUrl, 'http://localhost').pathname).replace(/\/+$/, '')
18
-
19
- const deleteMatch = pathname.match(/^\/api\/v0\/users\/([^/]+)\/workspaces\/([^/]+)$/)
20
- if (deleteMatch && req.method === 'DELETE') {
21
- const userId = decodeURIComponent(deleteMatch[1])
22
- const workspaceId = decodeURIComponent(deleteMatch[2])
23
-
24
- if (!isBotUser(req)) {
25
- res.status(403).json({ error: 'Forbidden' })
26
- return
27
- }
28
-
29
- try {
30
- await ActionService.invoke('users/leave-workspace', {
31
- payload: { userId, workspaceId },
32
- req,
33
- })
34
- res.status(204).send()
35
- } catch (err) {
36
- log.error('[workspace-membership.api] remove error', undefined, err)
37
- res.status(err?.status ?? 500).json({ error: 'Internal Server Error' })
38
- }
39
- return
40
- }
41
-
42
- const postMatch = pathname.match(/^\/api\/v0\/users\/([^/]+)\/workspaces$/)
43
- if (postMatch && req.method === 'POST') {
44
- const userId = decodeURIComponent(postMatch[1])
45
- const workspaceId = req.body?.workspaceId
46
- const createdBy = req.body?.createdBy ?? userId
47
-
48
- if (!isBotUser(req)) {
49
- res.status(403).json({ error: 'Forbidden' })
50
- return
51
- }
52
-
53
- if (!workspaceId) {
54
- res.status(400).json({ error: 'workspaceId is required' })
55
- return
56
- }
57
-
58
- try {
59
- await ActionService.invoke('users/join-workspace', {
60
- payload: { targetUserId: userId, workspaceId, createdBy },
61
- req,
62
- })
63
- res.status(204).send()
64
- } catch (err) {
65
- log.error('[workspace-membership.api] add error', undefined, err)
66
- res.status(err?.status ?? 500).json({ error: 'Internal Server Error' })
67
- }
68
- return
69
- }
70
-
71
- res.status(404).json({ error: 'Not Found' })
72
- }