@ossy/users 1.6.0 → 1.8.0
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 +3 -3
- package/src/create-api-token.action.js +35 -0
- package/src/get-api-tokens.action.js +20 -0
- package/src/get-current-user-history.action.js +11 -0
- package/src/invalidate-api-token.action.js +16 -0
- package/src/join-workspace.action.js +18 -0
- package/src/leave-workspace.action.js +17 -0
- package/src/list.api.js +23 -0
- package/src/me-history.api.js +23 -0
- package/src/me-token.api.js +32 -0
- package/src/me-tokens.api.js +35 -0
- package/src/me.api.js +41 -0
- package/src/update-details.action.js +29 -0
- package/src/workspace-membership.api.js +72 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/users",
|
|
3
3
|
"description": "User domain — aggregate, events, and validators for the Ossy user model",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.js",
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"registry": "https://registry.npmjs.org"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@ossy/observability": "^1.
|
|
22
|
+
"@ossy/observability": "^1.3.0",
|
|
23
23
|
"nanoid": "^5.1.11"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"/src",
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "c6697078268867d88553ca0bac08faad5cea1546"
|
|
30
30
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|
package/src/list.api.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
}
|