@ossy/users 1.13.2 → 1.13.3
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/README.md +72 -0
- package/package.json +7 -5
- package/src/create-api-token.action.js +1 -1
- package/src/create-api-token.task.js +25 -7
- package/src/en.translations.json +14 -14
- package/src/get-api-tokens.action.js +1 -1
- package/src/get-api-tokens.task.js +6 -5
- package/src/get-current-user-history.action.js +1 -1
- package/src/get-current-user-history.task.js +3 -3
- package/src/invalidate-api-token.action.js +1 -1
- package/src/invalidate-api-token.task.js +1 -1
- package/src/join-workspace.action.js +1 -1
- package/src/join-workspace.task.js +1 -1
- package/src/leave-workspace.action.js +1 -1
- package/src/leave-workspace.task.js +1 -1
- package/src/schema-ids.js +3 -0
- package/src/server.js +1 -0
- package/src/sv.translations.json +14 -14
- package/src/update-details.action.js +1 -1
- package/src/update-details.task.js +1 -1
- package/src/user.aggregate.js +17 -29
- package/src/user.schema.js +11 -0
- package/src/users.events.js +38 -102
- package/src/{users.spec.js → users.integration.spec.js} +83 -78
- package/src/users.queries.js +14 -29
- package/src/users.startup.js +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @ossy/users
|
|
2
|
+
|
|
3
|
+
User domain package for the Ossy platform — event-sourced user entities, workspace membership, and user-scoped API token actions.
|
|
4
|
+
|
|
5
|
+
## What this package provides
|
|
6
|
+
|
|
7
|
+
| Primitive | File | Id / export |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Schema | `user.schema.js` | `@ossy/users/schema/user` |
|
|
10
|
+
| Schema ids | `schema-ids.js` | `UserSchema` |
|
|
11
|
+
| Aggregate | `user.aggregate.js` | `User` (`kind: 'entity'`, `SchemaId`) |
|
|
12
|
+
| Events | `users.events.js` | `UsersEvents.Created`, `SignInVerified`, `NameUpdated`, … |
|
|
13
|
+
| Queries | `users.queries.js` | `UsersQueries` |
|
|
14
|
+
| Startup | `users.startup.js` | Bot user bootstrap |
|
|
15
|
+
|
|
16
|
+
Client exports (`@ossy/users`) surface action metadata for the SDK. Server exports (`@ossy/users/server`) include the aggregate, events, queries, and validators.
|
|
17
|
+
|
|
18
|
+
## Actions
|
|
19
|
+
|
|
20
|
+
| Action | Access | Task |
|
|
21
|
+
|--------|--------|------|
|
|
22
|
+
| `@ossy/users/actions/update-details` | authenticated | `@ossy/users/tasks/update-details` |
|
|
23
|
+
| `@ossy/users/actions/get-current-user-history` | authenticated | `@ossy/users/tasks/get-current-user-history` |
|
|
24
|
+
| `@ossy/users/actions/join-workspace` | authenticated | `@ossy/users/tasks/join-workspace` |
|
|
25
|
+
| `@ossy/users/actions/leave-workspace` | authenticated | `@ossy/users/tasks/leave-workspace` |
|
|
26
|
+
| `@ossy/users/actions/get-api-tokens` | authenticated | `@ossy/users/tasks/get-api-tokens` |
|
|
27
|
+
| `@ossy/users/actions/create-api-token` | authenticated | `@ossy/users/tasks/create-api-token` |
|
|
28
|
+
| `@ossy/users/actions/invalidate-api-token` | authenticated | `@ossy/users/tasks/invalidate-api-token` |
|
|
29
|
+
|
|
30
|
+
API token tasks delegate to `@ossy/tokens` (`TokenEvents.Created` / revoke flows).
|
|
31
|
+
|
|
32
|
+
## User schema (ADR 0006)
|
|
33
|
+
|
|
34
|
+
| Schema | Id | Fields |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| User | `@ossy/users/schema/user` | `email`, `firstName`, `lastName` |
|
|
37
|
+
|
|
38
|
+
## Event model (ADR 0008)
|
|
39
|
+
|
|
40
|
+
User entity events use the universal envelope: `type` (schema id), `resourceId`, `event` (lifecycle name), `payload`, `createdBy`.
|
|
41
|
+
|
|
42
|
+
| Event | Effect |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `Created` | New user (`type: 'User'` or `'Bot'`) |
|
|
45
|
+
| `SignInVerified` | Sets `verifiedAt` on first verification |
|
|
46
|
+
| `NameUpdated` | Updates `firstName` / `lastName` |
|
|
47
|
+
| `WorkspaceJoined` | Adds workspace id to `workspaces[]` |
|
|
48
|
+
| `WorkspaceLeft` | Removes workspace id from `workspaces[]` |
|
|
49
|
+
|
|
50
|
+
`UsersEvents.SignedUp` was renamed to `UsersEvents.Created` to match the ADR 0008 lifecycle name.
|
|
51
|
+
|
|
52
|
+
## Server usage
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { Aggregate } from '@ossy/event-store'
|
|
56
|
+
import { User, UsersEvents, UserSchema } from '@ossy/users/server'
|
|
57
|
+
|
|
58
|
+
const created = UsersEvents.Created({ email, firstName, lastName })
|
|
59
|
+
const user = await Aggregate.Of(User, created).then(Aggregate.View())
|
|
60
|
+
|
|
61
|
+
UserSchema.user
|
|
62
|
+
// => '@ossy/users/schema/user'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Testing
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm test -w @ossy/users
|
|
69
|
+
npm run test:integration -w @ossy/users
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Unit coverage lives in package tests; `users.integration.spec.js` exercises actions against `@ossy/platform`.
|
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.13.
|
|
4
|
+
"version": "1.13.3",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.js",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"./server": "./src/server.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose"
|
|
14
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose",
|
|
15
|
+
"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.integration.config.js --verbose"
|
|
15
16
|
},
|
|
16
17
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
17
18
|
"license": "MIT",
|
|
@@ -28,12 +29,13 @@
|
|
|
28
29
|
"registry": "https://registry.npmjs.org"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"@ossy/observability": "^1.8.
|
|
32
|
+
"@ossy/observability": "^1.8.3",
|
|
33
|
+
"@ossy/schema": "^1.0.1",
|
|
32
34
|
"nanoid": "^5.1.11"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
37
|
"@jest/globals": "^30.2.0",
|
|
36
|
-
"@ossy/platform": "^1.39.
|
|
38
|
+
"@ossy/platform": "^1.39.3",
|
|
37
39
|
"casual": "^1.6.2",
|
|
38
40
|
"jest": "^30.2.0"
|
|
39
41
|
},
|
|
@@ -41,5 +43,5 @@
|
|
|
41
43
|
"/src",
|
|
42
44
|
"README.md"
|
|
43
45
|
],
|
|
44
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "a0d89185a17f8de8ce328c3a648c108ff1d61d8f"
|
|
45
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'users/create-api-token', access: 'authenticated' }
|
|
1
|
+
export const metadata = { id: '@ossy/users/actions/create-api-token', access: 'authenticated' }
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
1
2
|
import jwt from 'jsonwebtoken'
|
|
2
3
|
import { Aggregate } from '@ossy/event-store'
|
|
3
4
|
import { Token, TokenEvents } from '@ossy/tokens/server'
|
|
4
5
|
import { ConfigService } from '@ossy/platform'
|
|
5
6
|
import { createLogger } from '@ossy/observability'
|
|
7
|
+
import { normalizeScopes } from '@ossy/schema'
|
|
6
8
|
|
|
7
|
-
export const metadata = { id: 'users/create-api-token' }
|
|
9
|
+
export const metadata = { id: '@ossy/users/tasks/create-api-token' }
|
|
8
10
|
|
|
9
|
-
const log = createLogger('users/create-api-token')
|
|
11
|
+
const log = createLogger('@ossy/users/tasks/create-api-token')
|
|
10
12
|
|
|
11
13
|
const SECONDS_IN_100_YEARS = 60 * 60 * 24 * 365 * 100
|
|
12
14
|
|
|
@@ -16,19 +18,35 @@ export async function run({ payload, req }) {
|
|
|
16
18
|
const description = payload?.description
|
|
17
19
|
|
|
18
20
|
if (!name || typeof name !== 'string') {
|
|
19
|
-
log.debug('[users/create-api-token] No name provided')
|
|
21
|
+
log.debug('[users/tasks/create-api-token] No name provided')
|
|
20
22
|
throw Object.assign(new Error('No name provided'), { status: 400 })
|
|
21
23
|
}
|
|
22
24
|
if (!description || typeof description !== 'string') {
|
|
23
|
-
log.debug('[users/create-api-token] No description provided')
|
|
25
|
+
log.debug('[users/tasks/create-api-token] No description provided')
|
|
24
26
|
throw Object.assign(new Error('No description provided'), { status: 400 })
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const expiresIn = SECONDS_IN_100_YEARS
|
|
28
30
|
const expiresAt = Date.now() + expiresIn * 1000
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
31
|
+
const scopes = normalizeScopes(payload?.scopes)
|
|
32
|
+
const resourceId = nanoid()
|
|
33
|
+
const token = jwt.sign(
|
|
34
|
+
{ sub: createdBy, type: 'Api', jti: resourceId, scopes },
|
|
35
|
+
ConfigService.TokenSecret,
|
|
36
|
+
{ expiresIn },
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const event = TokenEvents.Created({
|
|
40
|
+
resourceId,
|
|
41
|
+
name,
|
|
42
|
+
description,
|
|
43
|
+
createdBy,
|
|
44
|
+
type: 'Api',
|
|
45
|
+
subject: createdBy,
|
|
46
|
+
token,
|
|
47
|
+
expiresAt,
|
|
48
|
+
scopes,
|
|
49
|
+
})
|
|
32
50
|
|
|
33
51
|
return Aggregate.Of(Token, event).then(Aggregate.View())
|
|
34
52
|
}
|
package/src/en.translations.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"users/create-api-token.label": "Create API token",
|
|
3
|
-
"users/create-api-token.description": "Create a personal API token for the authenticated user",
|
|
4
|
-
"users/get-api-tokens.label": "Get API tokens",
|
|
5
|
-
"users/get-api-tokens.description": "List API tokens for the authenticated user",
|
|
6
|
-
"users/get-current-user-history.label": "Get user history",
|
|
7
|
-
"users/get-current-user-history.description": "Return activity history for the current user",
|
|
8
|
-
"users/invalidate-api-token.label": "Invalidate API token",
|
|
9
|
-
"users/invalidate-api-token.description": "Revoke an API token by id",
|
|
10
|
-
"users/join-workspace.label": "Join workspace",
|
|
11
|
-
"users/join-workspace.description": "Add the current user to a workspace",
|
|
12
|
-
"users/leave-workspace.label": "Leave workspace",
|
|
13
|
-
"users/leave-workspace.description": "Remove the current user from a workspace",
|
|
14
|
-
"users/update-details.label": "Update user details",
|
|
15
|
-
"users/update-details.description": "Update profile fields for the authenticated user"
|
|
2
|
+
"@ossy/users/actions/create-api-token.label": "Create API token",
|
|
3
|
+
"@ossy/users/actions/create-api-token.description": "Create a personal API token for the authenticated user",
|
|
4
|
+
"@ossy/users/actions/get-api-tokens.label": "Get API tokens",
|
|
5
|
+
"@ossy/users/actions/get-api-tokens.description": "List API tokens for the authenticated user",
|
|
6
|
+
"@ossy/users/actions/get-current-user-history.label": "Get user history",
|
|
7
|
+
"@ossy/users/actions/get-current-user-history.description": "Return activity history for the current user",
|
|
8
|
+
"@ossy/users/actions/invalidate-api-token.label": "Invalidate API token",
|
|
9
|
+
"@ossy/users/actions/invalidate-api-token.description": "Revoke an API token by id",
|
|
10
|
+
"@ossy/users/actions/join-workspace.label": "Join workspace",
|
|
11
|
+
"@ossy/users/actions/join-workspace.description": "Add the current user to a workspace",
|
|
12
|
+
"@ossy/users/actions/leave-workspace.label": "Leave workspace",
|
|
13
|
+
"@ossy/users/actions/leave-workspace.description": "Remove the current user from a workspace",
|
|
14
|
+
"@ossy/users/actions/update-details.label": "Update user details",
|
|
15
|
+
"@ossy/users/actions/update-details.description": "Update profile fields for the authenticated user"
|
|
16
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'users/get-api-tokens', access: 'authenticated' }
|
|
1
|
+
export const metadata = { id: '@ossy/users/actions/get-api-tokens', access: 'authenticated' }
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { TokenSchema } from '@ossy/tokens/server'
|
|
2
3
|
import { createLogger } from '@ossy/observability'
|
|
3
4
|
|
|
4
|
-
export const metadata = { id: 'users/get-api-tokens' }
|
|
5
|
+
export const metadata = { id: '@ossy/users/tasks/get-api-tokens' }
|
|
5
6
|
|
|
6
|
-
const log = createLogger('users/get-api-tokens')
|
|
7
|
+
const log = createLogger('@ossy/users/tasks/get-api-tokens')
|
|
7
8
|
|
|
8
|
-
export async function run({ payload, req }) {
|
|
9
|
+
export async function run ({ payload, req }) {
|
|
9
10
|
const userId = payload?.userId ?? req?.userId
|
|
10
11
|
|
|
11
|
-
log.info('[users/get-api-tokens] Fetching API tokens for user')
|
|
12
|
+
log.info('[users/tasks/get-api-tokens] Fetching API tokens for user')
|
|
12
13
|
const tokens = await Aggregate.Collection.find({
|
|
13
|
-
type:
|
|
14
|
+
type: TokenSchema.token,
|
|
14
15
|
'state.subject': userId,
|
|
15
16
|
'state.type': 'Api',
|
|
16
17
|
}, { state: true }).toArray().then(docs => docs.map(d => d.state))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'users/get-current-user-history', access: 'authenticated' }
|
|
1
|
+
export const metadata = { id: '@ossy/users/actions/get-current-user-history', access: 'authenticated' }
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { EventStore } from '@ossy/event-store'
|
|
2
2
|
import { User } from '@ossy/users/server'
|
|
3
3
|
|
|
4
|
-
export const metadata = { id: 'users/get-current-user-history' }
|
|
4
|
+
export const metadata = { id: '@ossy/users/tasks/get-current-user-history' }
|
|
5
5
|
|
|
6
|
-
export async function run({ payload, req }) {
|
|
6
|
+
export async function run ({ payload, req }) {
|
|
7
7
|
const userId = payload?.userId ?? req?.userId
|
|
8
|
-
const events = await EventStore.
|
|
8
|
+
const events = await EventStore.GetResourceStream({ resourceId: userId, fromVersion: 0 })
|
|
9
9
|
return User.History(events)
|
|
10
10
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'users/invalidate-api-token', access: 'authenticated' }
|
|
1
|
+
export const metadata = { id: '@ossy/users/actions/invalidate-api-token', access: 'authenticated' }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Aggregate } from '@ossy/event-store'
|
|
2
2
|
import { Token, TokenEvents } from '@ossy/tokens/server'
|
|
3
3
|
|
|
4
|
-
export const metadata = { id: 'users/invalidate-api-token' }
|
|
4
|
+
export const metadata = { id: '@ossy/users/tasks/invalidate-api-token' }
|
|
5
5
|
|
|
6
6
|
export async function run({ payload, req }) {
|
|
7
7
|
const createdBy = payload?.userId ?? req?.userId
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'users/join-workspace', access: 'authenticated' }
|
|
1
|
+
export const metadata = { id: '@ossy/users/actions/join-workspace', access: 'authenticated' }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Aggregate } from '@ossy/event-store'
|
|
2
2
|
import { User, UsersEvents } from '@ossy/users/server'
|
|
3
3
|
|
|
4
|
-
export const metadata = { id: 'users/join-workspace' }
|
|
4
|
+
export const metadata = { id: '@ossy/users/tasks/join-workspace' }
|
|
5
5
|
|
|
6
6
|
export async function run({ payload, req }) {
|
|
7
7
|
const userId = payload?.targetUserId ?? payload?.userId ?? req?.userId
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'users/leave-workspace', access: 'authenticated' }
|
|
1
|
+
export const metadata = { id: '@ossy/users/actions/leave-workspace', access: 'authenticated' }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Aggregate } from '@ossy/event-store'
|
|
2
2
|
import { User, UsersEvents } from '@ossy/users/server'
|
|
3
3
|
|
|
4
|
-
export const metadata = { id: 'users/leave-workspace' }
|
|
4
|
+
export const metadata = { id: '@ossy/users/tasks/leave-workspace' }
|
|
5
5
|
|
|
6
6
|
export async function run({ payload, req }) {
|
|
7
7
|
const userId = payload?.userId ?? req?.userId
|
package/src/server.js
CHANGED
package/src/sv.translations.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"users/create-api-token.label": "Skapa API-nyckel",
|
|
3
|
-
"users/create-api-token.description": "Skapa en personlig API-nyckel för den inloggade användaren",
|
|
4
|
-
"users/get-api-tokens.label": "Hämta API-nycklar",
|
|
5
|
-
"users/get-api-tokens.description": "Lista API-nycklar för den inloggade användaren",
|
|
6
|
-
"users/get-current-user-history.label": "Hämta användarhistorik",
|
|
7
|
-
"users/get-current-user-history.description": "Returnera aktivitetshistorik för nuvarande användare",
|
|
8
|
-
"users/invalidate-api-token.label": "Ogiltigförklara API-nyckel",
|
|
9
|
-
"users/invalidate-api-token.description": "Återkalla en API-nyckel via id",
|
|
10
|
-
"users/join-workspace.label": "Gå med i workspace",
|
|
11
|
-
"users/join-workspace.description": "Lägg till nuvarande användare i ett workspace",
|
|
12
|
-
"users/leave-workspace.label": "Lämna workspace",
|
|
13
|
-
"users/leave-workspace.description": "Ta bort nuvarande användare från ett workspace",
|
|
14
|
-
"users/update-details.label": "Uppdatera användaruppgifter",
|
|
15
|
-
"users/update-details.description": "Uppdatera profilfält för den inloggade användaren"
|
|
2
|
+
"@ossy/users/actions/create-api-token.label": "Skapa API-nyckel",
|
|
3
|
+
"@ossy/users/actions/create-api-token.description": "Skapa en personlig API-nyckel för den inloggade användaren",
|
|
4
|
+
"@ossy/users/actions/get-api-tokens.label": "Hämta API-nycklar",
|
|
5
|
+
"@ossy/users/actions/get-api-tokens.description": "Lista API-nycklar för den inloggade användaren",
|
|
6
|
+
"@ossy/users/actions/get-current-user-history.label": "Hämta användarhistorik",
|
|
7
|
+
"@ossy/users/actions/get-current-user-history.description": "Returnera aktivitetshistorik för nuvarande användare",
|
|
8
|
+
"@ossy/users/actions/invalidate-api-token.label": "Ogiltigförklara API-nyckel",
|
|
9
|
+
"@ossy/users/actions/invalidate-api-token.description": "Återkalla en API-nyckel via id",
|
|
10
|
+
"@ossy/users/actions/join-workspace.label": "Gå med i workspace",
|
|
11
|
+
"@ossy/users/actions/join-workspace.description": "Lägg till nuvarande användare i ett workspace",
|
|
12
|
+
"@ossy/users/actions/leave-workspace.label": "Lämna workspace",
|
|
13
|
+
"@ossy/users/actions/leave-workspace.description": "Ta bort nuvarande användare från ett workspace",
|
|
14
|
+
"@ossy/users/actions/update-details.label": "Uppdatera användaruppgifter",
|
|
15
|
+
"@ossy/users/actions/update-details.description": "Uppdatera profilfält för den inloggade användaren"
|
|
16
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'users/update-details', access: 'authenticated' }
|
|
1
|
+
export const metadata = { id: '@ossy/users/actions/update-details', access: 'authenticated' }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Aggregate } from '@ossy/event-store'
|
|
2
2
|
import { User, UsersEvents } from '@ossy/users/server'
|
|
3
3
|
|
|
4
|
-
export const metadata = { id: 'users/update-details' }
|
|
4
|
+
export const metadata = { id: '@ossy/users/tasks/update-details' }
|
|
5
5
|
|
|
6
6
|
export async function run({ payload, req }) {
|
|
7
7
|
const createdBy = payload?.userId ?? req?.userId
|
package/src/user.aggregate.js
CHANGED
|
@@ -1,85 +1,73 @@
|
|
|
1
|
+
import { UserSchema } from './schema-ids.js'
|
|
2
|
+
|
|
1
3
|
export class User {
|
|
2
4
|
|
|
5
|
+
static kind = 'entity'
|
|
6
|
+
static SchemaId = UserSchema.user
|
|
3
7
|
static AggregateType = 'User'
|
|
4
8
|
|
|
5
|
-
static View(events, savedState = {}) {
|
|
9
|
+
static View (events, savedState = {}) {
|
|
6
10
|
return events.reduce((user, event) => {
|
|
11
|
+
switch (event.event) {
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
case 'SignedUp':
|
|
13
|
+
case 'Created':
|
|
11
14
|
return {
|
|
12
15
|
...user,
|
|
13
|
-
id: event.
|
|
16
|
+
id: event.resourceId,
|
|
14
17
|
type: event.payload.type,
|
|
15
18
|
created: event.created,
|
|
16
19
|
email: event.payload.email,
|
|
17
|
-
verificationToken: event.payload.verificationToken,
|
|
18
20
|
firstName: event.payload.firstName,
|
|
19
21
|
lastName: event.payload.lastName,
|
|
20
|
-
workspaces: user.workspaces ?? []
|
|
22
|
+
workspaces: user.workspaces ?? [],
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
case 'SignInVerified':
|
|
24
26
|
return {
|
|
25
27
|
...user,
|
|
26
|
-
verifiedAt: user.verifiedAt ?? event.created
|
|
28
|
+
verifiedAt: user.verifiedAt ?? event.created,
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
case 'NameUpdated': {
|
|
30
32
|
const firstName = event.payload.firstName || user.firstName
|
|
31
33
|
const lastName = event.payload.lastName || user.lastName
|
|
32
|
-
return {
|
|
33
|
-
...user,
|
|
34
|
-
firstName,
|
|
35
|
-
lastName
|
|
36
|
-
}
|
|
34
|
+
return { ...user, firstName, lastName }
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
// TODO: #415
|
|
40
|
-
// case 'EmailUpdated':
|
|
41
|
-
// return {
|
|
42
|
-
// ...user,
|
|
43
|
-
// email: event.payload.email
|
|
44
|
-
// }
|
|
45
|
-
|
|
46
37
|
case 'WorkspaceJoined':
|
|
47
38
|
return {
|
|
48
39
|
...user,
|
|
49
|
-
workspaces: [...new Set([...(user.workspaces ?? []), event.payload.workspaceId])]
|
|
40
|
+
workspaces: [...new Set([...(user.workspaces ?? []), event.payload.workspaceId])],
|
|
50
41
|
}
|
|
51
42
|
|
|
52
43
|
case 'WorkspaceLeft':
|
|
53
44
|
return {
|
|
54
45
|
...user,
|
|
55
|
-
workspaces: (user.workspaces ?? []).filter(id => id !== event.payload.workspaceId)
|
|
46
|
+
workspaces: (user.workspaces ?? []).filter(id => id !== event.payload.workspaceId),
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
default:
|
|
59
50
|
return user
|
|
60
|
-
|
|
61
51
|
}
|
|
62
52
|
}, savedState)
|
|
63
53
|
}
|
|
64
54
|
|
|
65
|
-
static UserForWorkspace(events, aggregate) {
|
|
55
|
+
static UserForWorkspace (events, aggregate) {
|
|
66
56
|
const user = User.View(events, aggregate)
|
|
67
57
|
return {
|
|
68
58
|
id: user.id,
|
|
69
59
|
firstName: user.firstName,
|
|
70
60
|
lastName: user.lastName,
|
|
71
61
|
email: user.email,
|
|
72
|
-
type: user.type
|
|
62
|
+
type: user.type,
|
|
73
63
|
}
|
|
74
64
|
}
|
|
75
65
|
|
|
76
|
-
static History(events) {
|
|
77
|
-
// TODO: add aggregate
|
|
66
|
+
static History (events) {
|
|
78
67
|
return events
|
|
79
68
|
.sort((a, b) => b.created - a.created)
|
|
80
|
-
.map(event => ({ type: event.
|
|
69
|
+
.map(event => ({ type: event.event, created: event.created }))
|
|
81
70
|
}
|
|
82
|
-
|
|
83
71
|
}
|
|
84
72
|
|
|
85
73
|
export { User as Aggregate }
|
package/src/users.events.js
CHANGED
|
@@ -1,121 +1,57 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
|
+
import { UserSchema } from './schema-ids.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
5
|
+
* ADR 0008 user entity events.
|
|
6
|
+
*/
|
|
7
7
|
export class UsersEvents {
|
|
8
8
|
|
|
9
|
-
static
|
|
9
|
+
static Created ({
|
|
10
10
|
email,
|
|
11
11
|
lastName,
|
|
12
12
|
firstName,
|
|
13
13
|
userId: _userId,
|
|
14
|
-
type = 'User'
|
|
14
|
+
type = 'User',
|
|
15
15
|
}) {
|
|
16
|
-
const
|
|
17
|
-
return
|
|
18
|
-
type:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
type: type
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// static SignInRequested({ createdBy }) {
|
|
31
|
-
// return ({
|
|
32
|
-
// type: 'SignInRequested',
|
|
33
|
-
// createdBy: createdBy,
|
|
34
|
-
// payload: { verificationToken: TokenService.new() }
|
|
35
|
-
// })
|
|
36
|
-
// }
|
|
16
|
+
const resourceId = _userId ?? nanoid()
|
|
17
|
+
return {
|
|
18
|
+
type: UserSchema.user,
|
|
19
|
+
resourceId,
|
|
20
|
+
event: 'Created',
|
|
21
|
+
createdBy: resourceId,
|
|
22
|
+
payload: { email, lastName, firstName, type },
|
|
23
|
+
}
|
|
24
|
+
}
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
type: 'SignInVerified',
|
|
26
|
+
static SignInVerified ({ createdBy, token }) {
|
|
27
|
+
return {
|
|
28
|
+
event: 'SignInVerified',
|
|
42
29
|
createdBy,
|
|
43
|
-
payload: { token }
|
|
44
|
-
}
|
|
30
|
+
payload: { token },
|
|
31
|
+
}
|
|
45
32
|
}
|
|
46
33
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// })
|
|
53
|
-
// }
|
|
54
|
-
|
|
55
|
-
// static ApiTokenCreated({ name, createdBy, description, token, expiresAt }) {
|
|
56
|
-
// return ({
|
|
57
|
-
// type: 'ApiTokenCreated',
|
|
58
|
-
// createdBy: createdBy,
|
|
59
|
-
// payload: {
|
|
60
|
-
// name,
|
|
61
|
-
// expiresAt: expiresAt,
|
|
62
|
-
// description: description,
|
|
63
|
-
// token: token
|
|
64
|
-
// }
|
|
65
|
-
// })
|
|
66
|
-
// }
|
|
67
|
-
|
|
68
|
-
// static ApiTokenInvalidated({ createdBy, tokenId }) {
|
|
69
|
-
// return ({
|
|
70
|
-
// type: 'ApiTokenInvalidated',
|
|
71
|
-
// createdBy: createdBy,
|
|
72
|
-
// payload: { tokenId }
|
|
73
|
-
// })
|
|
74
|
-
// }
|
|
75
|
-
|
|
76
|
-
static NameUpdated({ lastName, firstName, createdBy }) {
|
|
77
|
-
return ({
|
|
78
|
-
type: 'NameUpdated',
|
|
79
|
-
createdBy: createdBy,
|
|
80
|
-
payload: {
|
|
81
|
-
lastName,
|
|
82
|
-
firstName
|
|
83
|
-
}
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// TODO: #415
|
|
88
|
-
// static EmailChangeRequested({ email, createdBy }) {
|
|
89
|
-
// return ({
|
|
90
|
-
// type: 'EmailUpdated',
|
|
91
|
-
// createdBy: createdBy,
|
|
92
|
-
// payload: { email }
|
|
93
|
-
// })
|
|
94
|
-
// }
|
|
95
|
-
|
|
96
|
-
// TODO: #415
|
|
97
|
-
// static EmailUpdated({ email, createdBy }) {
|
|
98
|
-
// return ({
|
|
99
|
-
// type: 'EmailUpdated',
|
|
100
|
-
// createdBy: createdBy,
|
|
101
|
-
// payload: { email }
|
|
102
|
-
// })
|
|
103
|
-
// }
|
|
104
|
-
|
|
105
|
-
static WorkspaceJoined({ workspaceId, createdBy }) {
|
|
106
|
-
return ({
|
|
107
|
-
type: 'WorkspaceJoined',
|
|
108
|
-
createdBy,
|
|
109
|
-
payload: { workspaceId }
|
|
110
|
-
})
|
|
34
|
+
static NameUpdated ({ lastName, firstName, createdBy }) {
|
|
35
|
+
return {
|
|
36
|
+
event: 'NameUpdated',
|
|
37
|
+
createdBy,
|
|
38
|
+
payload: { lastName, firstName },
|
|
111
39
|
}
|
|
40
|
+
}
|
|
112
41
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
})
|
|
42
|
+
static WorkspaceJoined ({ workspaceId, createdBy }) {
|
|
43
|
+
return {
|
|
44
|
+
event: 'WorkspaceJoined',
|
|
45
|
+
createdBy,
|
|
46
|
+
payload: { workspaceId },
|
|
119
47
|
}
|
|
48
|
+
}
|
|
120
49
|
|
|
50
|
+
static WorkspaceLeft ({ workspaceId, createdBy }) {
|
|
51
|
+
return {
|
|
52
|
+
event: 'WorkspaceLeft',
|
|
53
|
+
createdBy,
|
|
54
|
+
payload: { workspaceId },
|
|
55
|
+
}
|
|
56
|
+
}
|
|
121
57
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import casual from 'casual'
|
|
2
2
|
import { TestUtil, getApiTestBaseUrl, getSetCookieHeader } from '@ossy/platform/test'
|
|
3
3
|
|
|
4
|
+
const USER_SCHEMA = '@ossy/users/schema/user'
|
|
5
|
+
const TOKEN_SCHEMA = '@ossy/tokens/schema/token'
|
|
6
|
+
|
|
4
7
|
const authHeaders = (token) => ({
|
|
5
8
|
'Content-Type': 'application/json',
|
|
6
9
|
Authorization: token,
|
|
@@ -29,31 +32,31 @@ function invalidSignUpPayload(invalidEmail) {
|
|
|
29
32
|
return o
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
describe('[authentication/sign-up]', () => {
|
|
35
|
+
describe('[authentication/actions/sign-up]', () => {
|
|
33
36
|
|
|
34
37
|
describe('given an email that is not already in use', () => {
|
|
35
38
|
|
|
36
39
|
it('must return OK 200', () => TestUtil.AssertActionResponse({
|
|
37
|
-
actionId: 'authentication/sign-up',
|
|
40
|
+
actionId: '@ossy/authentication/actions/sign-up',
|
|
38
41
|
body: TestUtil.signUpBody({ email: casual.email }),
|
|
39
42
|
expectedResponseStatus: 200,
|
|
40
43
|
expectedResponseBody: { ok: true }
|
|
41
44
|
}))
|
|
42
45
|
|
|
43
|
-
it('must produce a
|
|
46
|
+
it('must produce a Created event', async () => {
|
|
44
47
|
|
|
45
48
|
const email = casual.email
|
|
46
49
|
|
|
47
50
|
await TestUtil.AssertActionResponse({
|
|
48
|
-
actionId: 'authentication/sign-up',
|
|
51
|
+
actionId: '@ossy/authentication/actions/sign-up',
|
|
49
52
|
body: TestUtil.signUpBody({ email }),
|
|
50
53
|
expectedResponseStatus: 200,
|
|
51
54
|
expectedResponseBody: { ok: true }
|
|
52
55
|
})
|
|
53
56
|
|
|
54
57
|
await TestUtil.AssertEventExist({
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
type: USER_SCHEMA,
|
|
59
|
+
event: 'Created',
|
|
57
60
|
'payload.email': email
|
|
58
61
|
})
|
|
59
62
|
|
|
@@ -67,7 +70,7 @@ describe('[authentication/sign-up]', () => {
|
|
|
67
70
|
const email = casual.email
|
|
68
71
|
|
|
69
72
|
const signUpTestRequest = {
|
|
70
|
-
actionId: 'authentication/sign-up',
|
|
73
|
+
actionId: '@ossy/authentication/actions/sign-up',
|
|
71
74
|
body: TestUtil.signUpBody({ email }),
|
|
72
75
|
expectedResponseStatus: 200,
|
|
73
76
|
expectedResponseBody: { ok: true }
|
|
@@ -81,7 +84,7 @@ describe('[authentication/sign-up]', () => {
|
|
|
81
84
|
const email = `dup-${Date.now()}-${casual.email}`
|
|
82
85
|
|
|
83
86
|
const signUpTestRequest = {
|
|
84
|
-
actionId: 'authentication/sign-up',
|
|
87
|
+
actionId: '@ossy/authentication/actions/sign-up',
|
|
85
88
|
body: TestUtil.signUpBody({ email }),
|
|
86
89
|
expectedResponseStatus: 200,
|
|
87
90
|
expectedResponseBody: { ok: true }
|
|
@@ -91,8 +94,8 @@ describe('[authentication/sign-up]', () => {
|
|
|
91
94
|
await TestUtil.AssertActionResponse(signUpTestRequest)
|
|
92
95
|
|
|
93
96
|
const signUpEvents = await TestUtil.GetEvents({
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
type: USER_SCHEMA,
|
|
98
|
+
event: 'Created',
|
|
96
99
|
'payload.email': email
|
|
97
100
|
})
|
|
98
101
|
|
|
@@ -107,7 +110,7 @@ describe('[authentication/sign-up]', () => {
|
|
|
107
110
|
it(
|
|
108
111
|
`must return BAD REQUEST 400 for invalid email: ${invalidEmail}`,
|
|
109
112
|
() => TestUtil.AssertActionResponse({
|
|
110
|
-
actionId: 'authentication/sign-up',
|
|
113
|
+
actionId: '@ossy/authentication/actions/sign-up',
|
|
111
114
|
payload: invalidSignUpPayload(invalidEmail),
|
|
112
115
|
expectedResponseStatus: 400,
|
|
113
116
|
expectedResponseBody: { error: 'Invalid email' }
|
|
@@ -118,7 +121,7 @@ describe('[authentication/sign-up]', () => {
|
|
|
118
121
|
|
|
119
122
|
})
|
|
120
123
|
|
|
121
|
-
describe('[authentication/request-sign-in]', () => {
|
|
124
|
+
describe('[authentication/actions/request-sign-in]', () => {
|
|
122
125
|
|
|
123
126
|
describe('given an email that is in use', () => {
|
|
124
127
|
|
|
@@ -126,14 +129,14 @@ describe('[authentication/request-sign-in]', () => {
|
|
|
126
129
|
const email = casual.email
|
|
127
130
|
|
|
128
131
|
await TestUtil.AssertActionResponse({
|
|
129
|
-
actionId: 'authentication/sign-up',
|
|
132
|
+
actionId: '@ossy/authentication/actions/sign-up',
|
|
130
133
|
body: TestUtil.signUpBody({ email }),
|
|
131
134
|
expectedResponseStatus: 200,
|
|
132
135
|
expectedResponseBody: { ok: true }
|
|
133
136
|
})
|
|
134
137
|
|
|
135
138
|
await TestUtil.AssertActionResponse({
|
|
136
|
-
actionId: 'authentication/request-sign-in',
|
|
139
|
+
actionId: '@ossy/authentication/actions/request-sign-in',
|
|
137
140
|
payload: { email },
|
|
138
141
|
expectedResponseStatus: 200,
|
|
139
142
|
expectedResponseBody: { ok: true }
|
|
@@ -146,26 +149,26 @@ describe('[authentication/request-sign-in]', () => {
|
|
|
146
149
|
const email = casual.email
|
|
147
150
|
|
|
148
151
|
await TestUtil.AssertActionResponse({
|
|
149
|
-
actionId: 'authentication/sign-up',
|
|
152
|
+
actionId: '@ossy/authentication/actions/sign-up',
|
|
150
153
|
body: TestUtil.signUpBody({ email }),
|
|
151
154
|
expectedResponseStatus: 200,
|
|
152
155
|
expectedResponseBody: { ok: true }
|
|
153
156
|
})
|
|
154
157
|
|
|
155
158
|
await TestUtil.AssertActionResponse({
|
|
156
|
-
actionId: 'authentication/request-sign-in',
|
|
159
|
+
actionId: '@ossy/authentication/actions/request-sign-in',
|
|
157
160
|
payload: { email },
|
|
158
161
|
expectedResponseStatus: 200,
|
|
159
162
|
expectedResponseBody: { ok: true }
|
|
160
163
|
})
|
|
161
164
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
const createdEvent = await TestUtil.GetEvent({
|
|
166
|
+
type: USER_SCHEMA,
|
|
167
|
+
event: 'Created',
|
|
165
168
|
'payload.email': email
|
|
166
169
|
})
|
|
167
170
|
|
|
168
|
-
const jwt = await TestUtil.getLatestVerificationJwtForSubject(
|
|
171
|
+
const jwt = await TestUtil.getLatestVerificationJwtForSubject(createdEvent.resourceId)
|
|
169
172
|
expect(typeof jwt).toBe('string')
|
|
170
173
|
})
|
|
171
174
|
|
|
@@ -175,7 +178,7 @@ describe('[authentication/request-sign-in]', () => {
|
|
|
175
178
|
|
|
176
179
|
it('must, for security reasons, return an OK 200 response', async () => {
|
|
177
180
|
await TestUtil.AssertActionResponse({
|
|
178
|
-
actionId: 'authentication/request-sign-in',
|
|
181
|
+
actionId: '@ossy/authentication/actions/request-sign-in',
|
|
179
182
|
payload: { email: casual.email },
|
|
180
183
|
expectedResponseStatus: 200,
|
|
181
184
|
expectedResponseBody: { ok: true }
|
|
@@ -187,7 +190,7 @@ describe('[authentication/request-sign-in]', () => {
|
|
|
187
190
|
const verificationTokensBefore = await TestUtil.countVerificationTokenEvents()
|
|
188
191
|
|
|
189
192
|
await TestUtil.AssertActionResponse({
|
|
190
|
-
actionId: 'authentication/request-sign-in',
|
|
193
|
+
actionId: '@ossy/authentication/actions/request-sign-in',
|
|
191
194
|
payload: { email: casual.email },
|
|
192
195
|
expectedResponseStatus: 200,
|
|
193
196
|
expectedResponseBody: { ok: true }
|
|
@@ -207,7 +210,7 @@ describe('[authentication/request-sign-in]', () => {
|
|
|
207
210
|
it(
|
|
208
211
|
`must return BAD REQUEST 400 for invalid email: ${invalidEmail}`,
|
|
209
212
|
() => TestUtil.AssertActionResponse({
|
|
210
|
-
actionId: 'authentication/request-sign-in',
|
|
213
|
+
actionId: '@ossy/authentication/actions/request-sign-in',
|
|
211
214
|
payload: { email: invalidEmail },
|
|
212
215
|
expectedResponseStatus: 400,
|
|
213
216
|
expectedResponseBody: { error: 'No email provided' }
|
|
@@ -220,24 +223,24 @@ describe('[authentication/request-sign-in]', () => {
|
|
|
220
223
|
|
|
221
224
|
describe('[/users/verify-sign-in][GET]', () => {
|
|
222
225
|
|
|
223
|
-
describe('given a valid token created from a
|
|
226
|
+
describe('given a valid token created from a Created event', () => {
|
|
224
227
|
it('must return OK 200 and an auth cookie and must produce a SignInVerified event', async () => {
|
|
225
228
|
const email = casual.email
|
|
226
229
|
|
|
227
230
|
await TestUtil.AssertActionResponse({
|
|
228
|
-
actionId: 'authentication/sign-up',
|
|
231
|
+
actionId: '@ossy/authentication/actions/sign-up',
|
|
229
232
|
body: TestUtil.signUpBody({ email }),
|
|
230
233
|
expectedResponseStatus: 200,
|
|
231
234
|
expectedResponseBody: { ok: true }
|
|
232
235
|
})
|
|
233
236
|
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
+
const createdEvent = await TestUtil.GetEvent({
|
|
238
|
+
type: USER_SCHEMA,
|
|
239
|
+
event: 'Created',
|
|
237
240
|
'payload.email': email
|
|
238
241
|
})
|
|
239
242
|
|
|
240
|
-
const verificationJwt = await TestUtil.getLatestVerificationJwtForSubject(
|
|
243
|
+
const verificationJwt = await TestUtil.getLatestVerificationJwtForSubject(createdEvent.resourceId)
|
|
241
244
|
|
|
242
245
|
const response = await fetch(
|
|
243
246
|
`${getApiTestBaseUrl()}/users/verify-sign-in?token=${verificationJwt}`,
|
|
@@ -247,9 +250,9 @@ describe('[/users/verify-sign-in][GET]', () => {
|
|
|
247
250
|
const body = await response.json()
|
|
248
251
|
|
|
249
252
|
const signInVerifiedEvent = await TestUtil.GetEvent({
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
+
type: USER_SCHEMA,
|
|
254
|
+
resourceId: createdEvent.resourceId,
|
|
255
|
+
event: 'SignInVerified',
|
|
253
256
|
})
|
|
254
257
|
|
|
255
258
|
expect(body).toBe('')
|
|
@@ -264,26 +267,26 @@ describe('[/users/verify-sign-in][GET]', () => {
|
|
|
264
267
|
const email = casual.email
|
|
265
268
|
|
|
266
269
|
await TestUtil.AssertActionResponse({
|
|
267
|
-
actionId: 'authentication/sign-up',
|
|
270
|
+
actionId: '@ossy/authentication/actions/sign-up',
|
|
268
271
|
body: TestUtil.signUpBody({ email }),
|
|
269
272
|
expectedResponseStatus: 200,
|
|
270
273
|
expectedResponseBody: { ok: true }
|
|
271
274
|
})
|
|
272
275
|
|
|
273
276
|
await TestUtil.AssertActionResponse({
|
|
274
|
-
actionId: 'authentication/request-sign-in',
|
|
277
|
+
actionId: '@ossy/authentication/actions/request-sign-in',
|
|
275
278
|
payload: { email },
|
|
276
279
|
expectedResponseStatus: 200,
|
|
277
280
|
expectedResponseBody: { ok: true }
|
|
278
281
|
})
|
|
279
282
|
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
const createdEvent = await TestUtil.GetEvent({
|
|
284
|
+
type: USER_SCHEMA,
|
|
285
|
+
event: 'Created',
|
|
283
286
|
'payload.email': email
|
|
284
287
|
})
|
|
285
288
|
|
|
286
|
-
const verificationJwt = await TestUtil.getLatestVerificationJwtForSubject(
|
|
289
|
+
const verificationJwt = await TestUtil.getLatestVerificationJwtForSubject(createdEvent.resourceId)
|
|
287
290
|
|
|
288
291
|
const response = await fetch(
|
|
289
292
|
`${getApiTestBaseUrl()}/users/verify-sign-in?token=${verificationJwt}`,
|
|
@@ -293,9 +296,9 @@ describe('[/users/verify-sign-in][GET]', () => {
|
|
|
293
296
|
const body = await response.json()
|
|
294
297
|
|
|
295
298
|
const signInVerifiedEvent = await TestUtil.GetEvent({
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
+
type: USER_SCHEMA,
|
|
300
|
+
resourceId: createdEvent.resourceId,
|
|
301
|
+
event: 'SignInVerified',
|
|
299
302
|
})
|
|
300
303
|
|
|
301
304
|
expect(body).toBe('')
|
|
@@ -334,11 +337,11 @@ describe('[/users/verify-sign-in][GET]', () => {
|
|
|
334
337
|
|
|
335
338
|
})
|
|
336
339
|
|
|
337
|
-
describe('[authentication/get-current-user]', () => {
|
|
340
|
+
describe('[authentication/actions/get-current-user]', () => {
|
|
338
341
|
|
|
339
342
|
describe('given no auth token is provided', () => {
|
|
340
343
|
it('must return null', async () => {
|
|
341
|
-
const response = await TestUtil.InvokeAction({ actionId: 'authentication/get-current-user' })
|
|
344
|
+
const response = await TestUtil.InvokeAction({ actionId: '@ossy/authentication/actions/get-current-user' })
|
|
342
345
|
expect(response.status).toEqual(200)
|
|
343
346
|
await expect(response.json()).resolves.toBeNull()
|
|
344
347
|
})
|
|
@@ -349,7 +352,7 @@ describe('[authentication/get-current-user]', () => {
|
|
|
349
352
|
const user = await TestUtil.GetAuthenticatedTestUser()
|
|
350
353
|
|
|
351
354
|
const response = await TestUtil.InvokeAction({
|
|
352
|
-
actionId: 'authentication/get-current-user',
|
|
355
|
+
actionId: '@ossy/authentication/actions/get-current-user',
|
|
353
356
|
headers: authHeaders(user.token),
|
|
354
357
|
})
|
|
355
358
|
|
|
@@ -370,10 +373,10 @@ describe('[authentication/get-current-user]', () => {
|
|
|
370
373
|
})
|
|
371
374
|
})
|
|
372
375
|
|
|
373
|
-
describe('[users/get-current-user-history]', () => {
|
|
376
|
+
describe('[users/actions/get-current-user-history]', () => {
|
|
374
377
|
|
|
375
378
|
TestUtil.AssertActionAuthenticationNeeded({
|
|
376
|
-
actionId: 'users/get-current-user-history',
|
|
379
|
+
actionId: '@ossy/users/actions/get-current-user-history',
|
|
377
380
|
})
|
|
378
381
|
|
|
379
382
|
describe('given an auth token', () => {
|
|
@@ -381,7 +384,7 @@ describe('[users/get-current-user-history]', () => {
|
|
|
381
384
|
const user = await TestUtil.GetAuthenticatedTestUser()
|
|
382
385
|
|
|
383
386
|
const response = await TestUtil.InvokeAction({
|
|
384
|
-
actionId: 'users/get-current-user-history',
|
|
387
|
+
actionId: '@ossy/users/actions/get-current-user-history',
|
|
385
388
|
headers: authHeaders(user.token),
|
|
386
389
|
})
|
|
387
390
|
|
|
@@ -392,26 +395,26 @@ describe('[users/get-current-user-history]', () => {
|
|
|
392
395
|
|
|
393
396
|
expect(body.map(x => x.type)).toEqual([
|
|
394
397
|
'SignInVerified',
|
|
395
|
-
'
|
|
398
|
+
'Created'
|
|
396
399
|
])
|
|
397
400
|
|
|
398
401
|
})
|
|
399
402
|
})
|
|
400
403
|
})
|
|
401
404
|
|
|
402
|
-
describe('[users/create-api-token]', () => {
|
|
405
|
+
describe('[users/actions/create-api-token]', () => {
|
|
403
406
|
|
|
404
407
|
TestUtil.AssertActionAuthenticationNeeded({
|
|
405
|
-
actionId: 'users/create-api-token',
|
|
408
|
+
actionId: '@ossy/users/actions/create-api-token',
|
|
406
409
|
payload: { name: 't', description: 'd' }
|
|
407
410
|
})
|
|
408
411
|
|
|
409
412
|
describe('given a valid auth token', () => {
|
|
410
|
-
it('must return OK 200 with an API token and produce an
|
|
413
|
+
it('must return OK 200 with an API token and produce an Api token Created event', async () => {
|
|
411
414
|
const user = await TestUtil.GetAuthenticatedTestUser()
|
|
412
415
|
|
|
413
416
|
const response = await TestUtil.InvokeAction({
|
|
414
|
-
actionId: 'users/create-api-token',
|
|
417
|
+
actionId: '@ossy/users/actions/create-api-token',
|
|
415
418
|
headers: authHeaders(user.token),
|
|
416
419
|
payload: { name: 'cli', description: casual.sentence }
|
|
417
420
|
})
|
|
@@ -428,7 +431,7 @@ describe('[users/create-api-token]', () => {
|
|
|
428
431
|
description: event.payload.description,
|
|
429
432
|
token: event.payload.token,
|
|
430
433
|
created: event.created,
|
|
431
|
-
id: event.
|
|
434
|
+
id: event.resourceId,
|
|
432
435
|
type: 'Api',
|
|
433
436
|
name: 'cli',
|
|
434
437
|
})
|
|
@@ -448,7 +451,7 @@ describe('[users/create-api-token]', () => {
|
|
|
448
451
|
else payload = invalidDescription
|
|
449
452
|
|
|
450
453
|
const response = await TestUtil.InvokeAction({
|
|
451
|
-
actionId: 'users/create-api-token',
|
|
454
|
+
actionId: '@ossy/users/actions/create-api-token',
|
|
452
455
|
headers: authHeaders(user.token),
|
|
453
456
|
payload,
|
|
454
457
|
})
|
|
@@ -456,9 +459,11 @@ describe('[users/create-api-token]', () => {
|
|
|
456
459
|
const responseBody = await response.json()
|
|
457
460
|
|
|
458
461
|
const event = TestUtil.GetEvent({
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
type: '
|
|
462
|
+
type: TOKEN_SCHEMA,
|
|
463
|
+
event: 'Created',
|
|
464
|
+
'payload.type': 'Api',
|
|
465
|
+
'payload.subject': user.id,
|
|
466
|
+
createdBy: user.id,
|
|
462
467
|
})
|
|
463
468
|
|
|
464
469
|
expect(response.status).toEqual(400)
|
|
@@ -471,10 +476,10 @@ describe('[users/create-api-token]', () => {
|
|
|
471
476
|
|
|
472
477
|
})
|
|
473
478
|
|
|
474
|
-
describe('[users/get-api-tokens]', () => {
|
|
479
|
+
describe('[users/actions/get-api-tokens]', () => {
|
|
475
480
|
|
|
476
481
|
TestUtil.AssertActionAuthenticationNeeded({
|
|
477
|
-
actionId: 'users/get-api-tokens',
|
|
482
|
+
actionId: '@ossy/users/actions/get-api-tokens',
|
|
478
483
|
})
|
|
479
484
|
|
|
480
485
|
describe('given authentication token is provided', () => {
|
|
@@ -483,13 +488,13 @@ describe('[users/get-api-tokens]', () => {
|
|
|
483
488
|
const user = await TestUtil.GetAuthenticatedTestUser()
|
|
484
489
|
|
|
485
490
|
const token = await TestUtil.InvokeAction({
|
|
486
|
-
actionId: 'users/create-api-token',
|
|
491
|
+
actionId: '@ossy/users/actions/create-api-token',
|
|
487
492
|
headers: authHeaders(user.token),
|
|
488
493
|
payload: { name: 'list-test', description: casual.sentence }
|
|
489
494
|
}).then(response => response.json())
|
|
490
495
|
|
|
491
496
|
const response = await TestUtil.InvokeAction({
|
|
492
|
-
actionId: 'users/get-api-tokens',
|
|
497
|
+
actionId: '@ossy/users/actions/get-api-tokens',
|
|
493
498
|
headers: authHeaders(user.token),
|
|
494
499
|
})
|
|
495
500
|
|
|
@@ -516,7 +521,7 @@ describe('[users/get-api-tokens]', () => {
|
|
|
516
521
|
|
|
517
522
|
const createApiToken = async (user) => {
|
|
518
523
|
const token = await TestUtil.InvokeAction({
|
|
519
|
-
actionId: 'users/create-api-token',
|
|
524
|
+
actionId: '@ossy/users/actions/create-api-token',
|
|
520
525
|
headers: authHeaders(user.token),
|
|
521
526
|
payload: { name: 'delete-test', description: casual.sentence }
|
|
522
527
|
}).then(response => response.json())
|
|
@@ -524,21 +529,21 @@ const createApiToken = async (user) => {
|
|
|
524
529
|
return token
|
|
525
530
|
}
|
|
526
531
|
|
|
527
|
-
describe('[users/invalidate-api-token]', () => {
|
|
532
|
+
describe('[users/actions/invalidate-api-token]', () => {
|
|
528
533
|
|
|
529
534
|
TestUtil.AssertActionAuthenticationNeeded({
|
|
530
|
-
actionId: 'users/invalidate-api-token',
|
|
535
|
+
actionId: '@ossy/users/actions/invalidate-api-token',
|
|
531
536
|
payload: { tokenId: '123' },
|
|
532
537
|
})
|
|
533
538
|
|
|
534
539
|
describe('authentication with user token', () => {
|
|
535
540
|
|
|
536
|
-
it('must return OK 200 and produce
|
|
541
|
+
it('must return OK 200 and produce a Revoked event', async () => {
|
|
537
542
|
const user = await TestUtil.GetAuthenticatedTestUser()
|
|
538
543
|
const token = await createApiToken(user)
|
|
539
544
|
|
|
540
545
|
const response = await TestUtil.InvokeAction({
|
|
541
|
-
actionId: 'users/invalidate-api-token',
|
|
546
|
+
actionId: '@ossy/users/actions/invalidate-api-token',
|
|
542
547
|
headers: authHeaders(user.token),
|
|
543
548
|
payload: { tokenId: token.id },
|
|
544
549
|
})
|
|
@@ -546,9 +551,9 @@ describe('[users/invalidate-api-token]', () => {
|
|
|
546
551
|
const responseBody = await response.json()
|
|
547
552
|
|
|
548
553
|
const event = await TestUtil.GetEvent({
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
554
|
+
resourceId: token.id,
|
|
555
|
+
type: TOKEN_SCHEMA,
|
|
556
|
+
event: 'Revoked',
|
|
552
557
|
})
|
|
553
558
|
|
|
554
559
|
expect(response.status).toEqual(200)
|
|
@@ -562,13 +567,13 @@ describe('[users/invalidate-api-token]', () => {
|
|
|
562
567
|
const token = await createApiToken(user)
|
|
563
568
|
|
|
564
569
|
await TestUtil.InvokeAction({
|
|
565
|
-
actionId: 'users/invalidate-api-token',
|
|
570
|
+
actionId: '@ossy/users/actions/invalidate-api-token',
|
|
566
571
|
headers: authHeaders(user.token),
|
|
567
572
|
payload: { tokenId: token.id },
|
|
568
573
|
})
|
|
569
574
|
|
|
570
575
|
const response = await TestUtil.InvokeAction({
|
|
571
|
-
actionId: 'users/get-api-tokens',
|
|
576
|
+
actionId: '@ossy/users/actions/get-api-tokens',
|
|
572
577
|
headers: authHeaders(user.token),
|
|
573
578
|
})
|
|
574
579
|
|
|
@@ -582,12 +587,12 @@ describe('[users/invalidate-api-token]', () => {
|
|
|
582
587
|
|
|
583
588
|
describe('authentication with API token', () => {
|
|
584
589
|
|
|
585
|
-
it('must return OK 200 and produce
|
|
590
|
+
it('must return OK 200 and produce a Revoked event', async () => {
|
|
586
591
|
const user = await TestUtil.GetAuthenticatedTestUser()
|
|
587
592
|
const token = await createApiToken(user)
|
|
588
593
|
|
|
589
594
|
const response = await TestUtil.InvokeAction({
|
|
590
|
-
actionId: 'users/invalidate-api-token',
|
|
595
|
+
actionId: '@ossy/users/actions/invalidate-api-token',
|
|
591
596
|
headers: authHeaders(token.token),
|
|
592
597
|
payload: { tokenId: token.id },
|
|
593
598
|
})
|
|
@@ -595,9 +600,9 @@ describe('[users/invalidate-api-token]', () => {
|
|
|
595
600
|
const responseBody = await response.json()
|
|
596
601
|
|
|
597
602
|
const event = await TestUtil.GetEvent({
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
603
|
+
resourceId: token.id,
|
|
604
|
+
type: TOKEN_SCHEMA,
|
|
605
|
+
event: 'Revoked',
|
|
601
606
|
})
|
|
602
607
|
|
|
603
608
|
expect(response.status).toEqual(200)
|
|
@@ -611,13 +616,13 @@ describe('[users/invalidate-api-token]', () => {
|
|
|
611
616
|
const token = await createApiToken(user)
|
|
612
617
|
|
|
613
618
|
await TestUtil.InvokeAction({
|
|
614
|
-
actionId: 'users/invalidate-api-token',
|
|
619
|
+
actionId: '@ossy/users/actions/invalidate-api-token',
|
|
615
620
|
headers: authHeaders(token.token),
|
|
616
621
|
payload: { tokenId: token.id },
|
|
617
622
|
})
|
|
618
623
|
|
|
619
624
|
const response = await TestUtil.InvokeAction({
|
|
620
|
-
actionId: 'users/get-api-tokens',
|
|
625
|
+
actionId: '@ossy/users/actions/get-api-tokens',
|
|
621
626
|
headers: authHeaders(token.token),
|
|
622
627
|
})
|
|
623
628
|
|
package/src/users.queries.js
CHANGED
|
@@ -1,52 +1,37 @@
|
|
|
1
1
|
import { EventStore, Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { UserSchema } from './schema-ids.js'
|
|
2
3
|
import { createLogger } from '@ossy/observability'
|
|
3
4
|
|
|
4
5
|
const log = createLogger('users')
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
-
* Database queries related to user aggregates.
|
|
8
|
-
* @class
|
|
9
|
-
*/
|
|
10
7
|
export class UsersQueries {
|
|
11
8
|
|
|
12
|
-
static getByEmail(email) {
|
|
9
|
+
static getByEmail (email) {
|
|
13
10
|
log.debug(`[UsersQueries][getByEmail()] Fetch user for ${email}`)
|
|
14
11
|
|
|
15
12
|
return Aggregate.Collection.findOne({
|
|
16
|
-
type:
|
|
17
|
-
'state.email': email
|
|
13
|
+
type: UserSchema.user,
|
|
14
|
+
'state.email': email,
|
|
18
15
|
}).then(aggregate => aggregate?.state)
|
|
19
|
-
.then(user => {
|
|
20
|
-
!!user
|
|
21
|
-
? log.debug('[UsersQueries][getByEmail()] Found user', { user })
|
|
22
|
-
: log.debug('[UsersQueries][getByEmail()] No user found')
|
|
23
|
-
|
|
24
|
-
return user
|
|
25
|
-
})
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
static getIdByVerificationToken(token) {
|
|
18
|
+
static getIdByVerificationToken (token) {
|
|
29
19
|
log.info('[UsersQueries] Using verificationToken to fetch userId')
|
|
30
20
|
|
|
31
21
|
return EventStore.FindEvent({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'payload.verificationToken': token
|
|
35
|
-
}).then(event => event
|
|
22
|
+
type: UserSchema.user,
|
|
23
|
+
event: 'Created',
|
|
24
|
+
'payload.verificationToken': token,
|
|
25
|
+
}).then(event => event?.resourceId)
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
static getIdByAuthToken(token) {
|
|
28
|
+
static getIdByAuthToken (token) {
|
|
39
29
|
log.info('[UsersQueries] Using authToken to fetch userId')
|
|
40
|
-
log.debug('[UsersQueries] token', { token })
|
|
41
30
|
|
|
42
31
|
return EventStore.FindEvent({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'payload.token': token
|
|
46
|
-
}).then(event =>
|
|
47
|
-
log.debug('[UsersQueries] Found', { event })
|
|
48
|
-
return event.aggregateId
|
|
49
|
-
})
|
|
32
|
+
type: UserSchema.user,
|
|
33
|
+
event: 'SignInVerified',
|
|
34
|
+
'payload.token': token,
|
|
35
|
+
}).then(event => event?.resourceId)
|
|
50
36
|
}
|
|
51
|
-
|
|
52
37
|
}
|
package/src/users.startup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Aggregate } from '@ossy/event-store'
|
|
2
|
-
import { UsersEvents, User } from './server.js'
|
|
2
|
+
import { UsersEvents, User, UserSchema } from './server.js'
|
|
3
3
|
import { createLogger } from '@ossy/observability'
|
|
4
4
|
|
|
5
5
|
const log = createLogger('users')
|
|
@@ -19,7 +19,7 @@ export async function run ({ env }) {
|
|
|
19
19
|
log.info('[users.startup][create-bot-user] Starting bot user creation script')
|
|
20
20
|
|
|
21
21
|
const existing = await Aggregate.Collection.findOne({
|
|
22
|
-
type:
|
|
22
|
+
type: UserSchema.user,
|
|
23
23
|
'state.email': botEmail,
|
|
24
24
|
})
|
|
25
25
|
|
|
@@ -30,12 +30,12 @@ export async function run ({ env }) {
|
|
|
30
30
|
|
|
31
31
|
log.info('[users.startup][create-bot-user] No bot user found, creating')
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const createdEvent = UsersEvents.Created({
|
|
34
34
|
type: 'Bot',
|
|
35
35
|
email: botEmail,
|
|
36
36
|
userId: botUserId,
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
await Aggregate.Of(User,
|
|
39
|
+
await Aggregate.Of(User, createdEvent)
|
|
40
40
|
log.info('[users.startup][create-bot-user] Bot user created')
|
|
41
41
|
}
|