@ossy/workspaces 1.17.0 → 1.17.1
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 +1 -0
- package/package.json +5 -4
- package/src/AuthenticationGuard.jsx +17 -5
- package/src/CreateWorkspacePage.jsx +11 -7
- package/src/GeneralSettings.jsx +6 -3
- package/src/Invitations.jsx +4 -2
- package/src/Invite.jsx +6 -4
- package/src/SelectWorkspacePage.jsx +5 -3
- package/src/Users.jsx +4 -2
- package/src/accept-invitation.action.js +1 -85
- package/src/accept-invitation.task.js +84 -0
- package/src/create-api-token.action.js +1 -17
- package/src/create-api-token.task.js +16 -0
- package/src/create.action.js +1 -30
- package/src/create.task.js +29 -0
- package/src/disable-service.action.js +1 -28
- package/src/disable-service.task.js +27 -0
- package/src/en.translations.json +6 -6
- package/src/enable-service.action.js +1 -28
- package/src/enable-service.task.js +27 -0
- package/src/get-api-tokens.action.js +1 -11
- package/src/get-api-tokens.task.js +10 -0
- package/src/get-invitations.action.js +1 -11
- package/src/get-invitations.task.js +10 -0
- package/src/get-resource-templates.action.js +1 -19
- package/src/get-resource-templates.task.js +18 -0
- package/src/get-users.action.js +1 -14
- package/src/get-users.task.js +13 -0
- package/src/get.action.js +1 -14
- package/src/get.task.js +13 -0
- package/src/import-resource-templates.action.js +1 -27
- package/src/import-resource-templates.task.js +26 -0
- package/src/index.js +14 -2
- package/src/invite-user.action.js +1 -85
- package/src/invite-user.task.js +84 -0
- package/src/list.action.js +1 -9
- package/src/list.task.js +8 -0
- package/src/remove-member.action.js +1 -17
- package/src/remove-member.task.js +16 -0
- package/src/server.js +3 -0
- package/src/sv.translations.json +6 -6
- package/src/sync-workspace-membership.task.js +4 -3
- package/src/workspaces.spec.js +71 -80
- package/src/current.api.js +0 -30
- package/src/invitations.api.js +0 -46
- package/src/item.api.js +0 -32
- package/src/list.api.js +0 -35
- package/src/members.api.js +0 -42
- package/src/resource-templates.api.js +0 -35
- package/src/services-disable.api.js +0 -23
- package/src/services-enable.api.js +0 -23
- package/src/tokens.api.js +0 -35
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Workspaces **feature package** for the Ossy platform — provides workspace crea
|
|
|
26
26
|
| `AuthenticationGuard` | Wraps children, shows error for unauthenticated users |
|
|
27
27
|
| `patchUserWorkspaceId` | Utility to update the active workspace cookie |
|
|
28
28
|
| `Definition` | Feature metadata (id, actions, views) |
|
|
29
|
+
| `ListWorkspaces`, `GetWorkspace`, … | Platform action POJOs — use with `useSdk().read()` / `invoke()` |
|
|
29
30
|
|
|
30
31
|
## Usage
|
|
31
32
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/workspaces",
|
|
3
|
-
"description": "Workspaces feature package
|
|
4
|
-
"version": "1.17.
|
|
3
|
+
"description": "Workspaces feature package - create, select, and manage workspaces, users, and invitations",
|
|
4
|
+
"version": "1.17.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.js",
|
|
10
|
+
"./server": "./src/server.js",
|
|
10
11
|
"./workspace-invitation.email.jsx": "./src/workspace-invitation.email.jsx"
|
|
11
12
|
},
|
|
12
13
|
"scripts": {
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@jest/globals": "^30.2.0",
|
|
28
|
-
"@ossy/platform": "^1.39.
|
|
29
|
+
"@ossy/platform": "^1.39.1",
|
|
29
30
|
"casual": "^1.6.2",
|
|
30
31
|
"jest": "^30.2.0"
|
|
31
32
|
},
|
|
@@ -37,5 +38,5 @@
|
|
|
37
38
|
"/src",
|
|
38
39
|
"README.md"
|
|
39
40
|
],
|
|
40
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "c0ba5d90749690634e4dc2705178ff8d89dd3070"
|
|
41
42
|
}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { AuthenticationStatus,
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import { AuthenticationStatus, GetCurrentUser } from '@ossy/authentication'
|
|
3
|
+
import { useSdk, AsyncStatus } from '@ossy/sdk-react'
|
|
3
4
|
import { Switch, View, Guide, Icon, useLocale } from '@ossy/design-system'
|
|
4
5
|
|
|
5
6
|
export const AuthenticationGuard = ({ children }) => {
|
|
6
7
|
const { t } = useLocale()
|
|
7
|
-
const
|
|
8
|
+
const sdk = useSdk()
|
|
9
|
+
const { status, data: user } = sdk.read(GetCurrentUser)
|
|
10
|
+
|
|
11
|
+
const authStatus = useMemo(() => {
|
|
12
|
+
if (status === AsyncStatus.Loading || status === AsyncStatus.NotInitialized) {
|
|
13
|
+
return AuthenticationStatus.Verifying
|
|
14
|
+
}
|
|
15
|
+
if (status === AsyncStatus.Success && user) {
|
|
16
|
+
return AuthenticationStatus.Authenticated
|
|
17
|
+
}
|
|
18
|
+
return AuthenticationStatus.NotAuthenticated
|
|
19
|
+
}, [status, user])
|
|
8
20
|
|
|
9
21
|
return (
|
|
10
|
-
<Switch on={
|
|
22
|
+
<Switch on={authStatus}>
|
|
11
23
|
|
|
12
24
|
<Switch.Case match={[AuthenticationStatus.NotAuthenticated]}>
|
|
13
25
|
<View layout="off-center" style={{ height: '100%' }}>
|
|
@@ -31,7 +43,7 @@ export const AuthenticationGuard = ({ children }) => {
|
|
|
31
43
|
</View>
|
|
32
44
|
</Switch.Case>
|
|
33
45
|
|
|
34
|
-
<Switch.Case match={[AuthenticationStatus.Verifying, AuthenticationStatus.
|
|
46
|
+
<Switch.Case match={[AuthenticationStatus.Verifying, AuthenticationStatus.NotInitialized]}>
|
|
35
47
|
<View layout="off-center" style={{ height: '100%' }}>
|
|
36
48
|
<Icon
|
|
37
49
|
name="spinner"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { ListWorkspaces, CreateWorkspace } from '@ossy/workspaces'
|
|
3
|
+
import { useSdk, cacheKey } from '@ossy/sdk-react'
|
|
3
4
|
import {
|
|
4
5
|
Button,
|
|
5
6
|
useInputValue,
|
|
@@ -11,24 +12,26 @@ import {
|
|
|
11
12
|
import { useRouter } from '@ossy/router-react'
|
|
12
13
|
import { AuthenticationGuard } from './AuthenticationGuard.jsx'
|
|
13
14
|
import { patchUserWorkspaceId } from './patchUserWorkspaceId.js'
|
|
15
|
+
import { metadata as CreateWorkspaceMeta } from './create.action.js'
|
|
14
16
|
|
|
15
17
|
export const CreateWorkspacePage = () => {
|
|
16
18
|
const router = useRouter()
|
|
17
19
|
const { t } = useLocale()
|
|
18
20
|
const [error, setError] = useState()
|
|
19
21
|
const [workspaceName, setWorkspaceName] = useInputValue()
|
|
20
|
-
const
|
|
22
|
+
const sdk = useSdk()
|
|
21
23
|
|
|
22
24
|
const onCreateWorkspace = workspaceName => {
|
|
23
|
-
|
|
24
|
-
.then((workspace) =>
|
|
25
|
-
|
|
25
|
+
sdk.invoke(CreateWorkspace, { name: workspaceName })
|
|
26
|
+
.then((workspace) => {
|
|
27
|
+
sdk.invalidate(cacheKey(ListWorkspaces))
|
|
28
|
+
return patchUserWorkspaceId(workspace.id).then((res) => {
|
|
26
29
|
if (!res.ok) throw new Error('Could not select the new workspace')
|
|
27
30
|
window.location.assign(
|
|
28
31
|
router.getHref({ id: '@storage/home', params: { workspaceId: workspace.id } })
|
|
29
32
|
)
|
|
30
33
|
})
|
|
31
|
-
)
|
|
34
|
+
})
|
|
32
35
|
.catch(err => { setError(err.message) })
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -36,7 +39,7 @@ export const CreateWorkspacePage = () => {
|
|
|
36
39
|
<AuthenticationGuard>
|
|
37
40
|
<View gap="m">
|
|
38
41
|
|
|
39
|
-
<title>{t('workspaces
|
|
42
|
+
<title>{t('profile/workspaces/create.documentTitle')}</title>
|
|
40
43
|
<Title variant="primary" className="stack-m">{t('workspaces.create.title')}</Title>
|
|
41
44
|
|
|
42
45
|
<Input
|
|
@@ -61,6 +64,7 @@ export const CreateWorkspacePage = () => {
|
|
|
61
64
|
|
|
62
65
|
<Button
|
|
63
66
|
id="createWorkspace"
|
|
67
|
+
data-action={CreateWorkspaceMeta.id}
|
|
64
68
|
variant="cta"
|
|
65
69
|
onClick={() => onCreateWorkspace(workspaceName)}>{t('workspaces.create.submit')}
|
|
66
70
|
</Button>
|
package/src/GeneralSettings.jsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { GetWorkspace } from '@ossy/workspaces'
|
|
3
|
+
import { GetCurrentUser } from '@ossy/authentication'
|
|
4
|
+
import { useSdk, AsyncStatus } from '@ossy/sdk-react'
|
|
3
5
|
import { Switch, Title, Button, View, Text, useLocale } from '@ossy/design-system'
|
|
4
6
|
|
|
5
7
|
const formatDate = (timestamp) => timestamp ? new Date(timestamp).toLocaleDateString() : ''
|
|
@@ -7,8 +9,9 @@ const FlowStage = { View: 'View', Edit: 'Edit' }
|
|
|
7
9
|
|
|
8
10
|
export const GeneralSettings = () => {
|
|
9
11
|
const { t } = useLocale()
|
|
10
|
-
const
|
|
11
|
-
const {
|
|
12
|
+
const sdk = useSdk()
|
|
13
|
+
const { data: workspace } = sdk.read(GetWorkspace)
|
|
14
|
+
const { status: userStatus } = sdk.read(GetCurrentUser)
|
|
12
15
|
const [flowStage, setFlowStage] = useState(FlowStage.View)
|
|
13
16
|
const services = workspace?.services || {}
|
|
14
17
|
|
package/src/Invitations.jsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { GetWorkspace } from '@ossy/workspaces'
|
|
3
|
+
import { useSdk } from '@ossy/sdk-react'
|
|
3
4
|
import { Title, Text, Button, View, Icon, useLocale } from '@ossy/design-system'
|
|
4
5
|
import { useRouter } from '@ossy/router-react'
|
|
5
6
|
|
|
6
7
|
export const Invitations = () => {
|
|
7
8
|
const { t } = useLocale()
|
|
8
9
|
const router = useRouter()
|
|
9
|
-
const
|
|
10
|
+
const sdk = useSdk()
|
|
11
|
+
const { data: workspace } = sdk.read(GetWorkspace)
|
|
10
12
|
const invitations = workspace?.invitations || []
|
|
11
13
|
|
|
12
14
|
return (
|
package/src/Invite.jsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useRef } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { InviteUser } from '@ossy/workspaces'
|
|
3
|
+
import { useSdk } from '@ossy/sdk-react'
|
|
3
4
|
import { useRouter } from '@ossy/router-react'
|
|
4
5
|
import {
|
|
5
6
|
useInputValue,
|
|
@@ -22,14 +23,14 @@ const FlowStage = {
|
|
|
22
23
|
export const Invite = () => {
|
|
23
24
|
const { t } = useLocale()
|
|
24
25
|
const router = useRouter()
|
|
25
|
-
const
|
|
26
|
+
const sdk = useSdk()
|
|
26
27
|
const [flowStage, setFlowStage] = useState(FlowStage.EnterEmail)
|
|
27
28
|
const [email, setEmail] = useInputValue('')
|
|
28
29
|
const formRef = useRef()
|
|
29
30
|
|
|
30
31
|
const sendInvite = event => {
|
|
31
32
|
event.preventDefault()
|
|
32
|
-
formRef.current.reportValidity() &&
|
|
33
|
+
formRef.current.reportValidity() && sdk.invoke(InviteUser, { email })
|
|
33
34
|
.then(() => setFlowStage(FlowStage.Success))
|
|
34
35
|
.catch(() => setFlowStage(FlowStage.Error))
|
|
35
36
|
}
|
|
@@ -37,7 +38,7 @@ export const Invite = () => {
|
|
|
37
38
|
return (
|
|
38
39
|
<Switch on={flowStage}>
|
|
39
40
|
|
|
40
|
-
<title>{t('workspace
|
|
41
|
+
<title>{t('workspace/invitations/add.documentTitle')}</title>
|
|
41
42
|
|
|
42
43
|
<Switch.Case match={[FlowStage.EnterEmail]}>
|
|
43
44
|
<View gap="m">
|
|
@@ -73,6 +74,7 @@ export const Invite = () => {
|
|
|
73
74
|
<Button
|
|
74
75
|
variant="cta"
|
|
75
76
|
id="registerButton"
|
|
77
|
+
data-action={InviteUser.id}
|
|
76
78
|
type="submit"
|
|
77
79
|
onClick={sendInvite}>{t('workspace.invite.submit')}
|
|
78
80
|
</Button>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { ListWorkspaces } from '@ossy/workspaces'
|
|
3
|
+
import { useSdk, AsyncStatus } from '@ossy/sdk-react'
|
|
3
4
|
import { Title, Text, Switch, Button, View, Guide, Icon, useLocale } from '@ossy/design-system'
|
|
4
5
|
import { useRouter } from '@ossy/router-react'
|
|
5
6
|
import { AuthenticationGuard } from './AuthenticationGuard.jsx'
|
|
@@ -15,7 +16,8 @@ const PageFlow = {
|
|
|
15
16
|
export const SelectWorkspacePage = () => {
|
|
16
17
|
const router = useRouter()
|
|
17
18
|
const { t } = useLocale()
|
|
18
|
-
const
|
|
19
|
+
const sdk = useSdk()
|
|
20
|
+
const { status: asyncStatus, data: workspaces = [] } = sdk.read(ListWorkspaces)
|
|
19
21
|
|
|
20
22
|
const pageFlow = (asyncStatus === AsyncStatus.Success && workspaces.length === 0)
|
|
21
23
|
? PageFlow.NoWorkspacesCreated
|
|
@@ -31,7 +33,7 @@ export const SelectWorkspacePage = () => {
|
|
|
31
33
|
return (
|
|
32
34
|
<AuthenticationGuard>
|
|
33
35
|
|
|
34
|
-
<title>{t('workspaces.
|
|
36
|
+
<title>{t('profile/workspaces.documentTitle')}</title>
|
|
35
37
|
|
|
36
38
|
<Switch on={pageFlow}>
|
|
37
39
|
<Switch.Case match={[PageFlow.Success]}>
|
package/src/Users.jsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { GetUsers } from '@ossy/workspaces'
|
|
3
|
+
import { useSdk, AsyncStatus } from '@ossy/sdk-react'
|
|
3
4
|
import { Text, View, Icon, Title, Button, useLocale } from '@ossy/design-system'
|
|
4
5
|
import { useRouter } from '@ossy/router-react'
|
|
5
6
|
|
|
6
7
|
export const Users = () => {
|
|
7
8
|
const { t } = useLocale()
|
|
8
9
|
const router = useRouter()
|
|
9
|
-
const
|
|
10
|
+
const sdk = useSdk()
|
|
11
|
+
const { status, data: users = [] } = sdk.read(GetUsers)
|
|
10
12
|
|
|
11
13
|
if (status === AsyncStatus.Loading) return <>{t('workspace.users.loading')}</>
|
|
12
14
|
|
|
@@ -1,85 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Aggregate } from '@ossy/event-store'
|
|
3
|
-
import { Workspace, WorkspacesEvents } from '@ossy/workspaces'
|
|
4
|
-
import { User, UsersEvents } from '@ossy/users'
|
|
5
|
-
import { Token, TokenEvents } from '@ossy/tokens'
|
|
6
|
-
import { ConfigService } from '@ossy/platform'
|
|
7
|
-
import { createLogger } from '@ossy/observability'
|
|
8
|
-
|
|
9
|
-
const log = createLogger('workspaces/accept-invitation')
|
|
10
|
-
|
|
11
|
-
function verifyToken(token) {
|
|
12
|
-
return new Promise((resolve, reject) => {
|
|
13
|
-
if (!token) return reject(new Error('No token'))
|
|
14
|
-
jwt.verify(token, ConfigService.TokenSecret, { algorithms: ['HS256'] }, (err, payload) => {
|
|
15
|
-
if (err) return reject(err)
|
|
16
|
-
resolve(payload)
|
|
17
|
-
})
|
|
18
|
-
})
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const id = 'workspaces/accept-invitation'
|
|
22
|
-
export const access = 'public'
|
|
23
|
-
|
|
24
|
-
export async function run({ payload, req }) {
|
|
25
|
-
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
26
|
-
const verificationToken = payload?.token ?? req?.query?.token
|
|
27
|
-
|
|
28
|
-
const { aud: tokenWorkspaceId, sub: email } = await verifyToken(verificationToken)
|
|
29
|
-
.catch(() => { throw Object.assign(new Error('Invalid token'), { status: 401 }) })
|
|
30
|
-
|
|
31
|
-
const resolvedWorkspaceId = tokenWorkspaceId ?? workspaceId
|
|
32
|
-
const workspace = await Aggregate.Of(Workspace, resolvedWorkspaceId).then(Aggregate.View())
|
|
33
|
-
|
|
34
|
-
if (!email) throw Object.assign(new Error('No email found in token payload'), { status: 401 })
|
|
35
|
-
|
|
36
|
-
const existingUser = await Aggregate.Collection.findOne({ type: 'User', 'state.email': email }).then(agg => agg?.state)
|
|
37
|
-
|
|
38
|
-
if (existingUser && workspace.users.includes(existingUser.id)) {
|
|
39
|
-
throw Object.assign(new Error('User already in workspace'), { status: 400 })
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let userId
|
|
43
|
-
let signInToken
|
|
44
|
-
let signInExpiresAt
|
|
45
|
-
|
|
46
|
-
if (existingUser) {
|
|
47
|
-
userId = existingUser.id
|
|
48
|
-
const signInExpiresIn = ConfigService.TokenValidity
|
|
49
|
-
signInExpiresAt = Date.now() + signInExpiresIn * 1000
|
|
50
|
-
signInToken = jwt.sign({ sub: userId, type: 'WebAuth' }, ConfigService.TokenSecret, { expiresIn: signInExpiresIn })
|
|
51
|
-
const signInEvent = TokenEvents.Created({
|
|
52
|
-
type: 'WebAuth',
|
|
53
|
-
subject: userId,
|
|
54
|
-
createdBy: userId,
|
|
55
|
-
token: signInToken,
|
|
56
|
-
expiresAt: signInExpiresAt,
|
|
57
|
-
})
|
|
58
|
-
const userInvitationAccepted = WorkspacesEvents.UserInvitationAccepted({ email, createdBy: userId })
|
|
59
|
-
await Promise.all([
|
|
60
|
-
Aggregate.Of(Token, signInEvent),
|
|
61
|
-
Aggregate.Of(Workspace, resolvedWorkspaceId).then(Aggregate.Add(userInvitationAccepted)),
|
|
62
|
-
])
|
|
63
|
-
} else {
|
|
64
|
-
const signUpEvent = UsersEvents.SignedUp({ email })
|
|
65
|
-
userId = signUpEvent.createdBy
|
|
66
|
-
const signInExpiresIn = ConfigService.TokenValidity
|
|
67
|
-
signInExpiresAt = Date.now() + signInExpiresIn * 1000
|
|
68
|
-
signInToken = jwt.sign({ sub: userId, type: 'WebAuth' }, ConfigService.TokenSecret, { expiresIn: signInExpiresIn })
|
|
69
|
-
const signInEvent = TokenEvents.Created({
|
|
70
|
-
type: 'WebAuth',
|
|
71
|
-
subject: userId,
|
|
72
|
-
createdBy: userId,
|
|
73
|
-
token: signInToken,
|
|
74
|
-
expiresAt: signInExpiresAt,
|
|
75
|
-
})
|
|
76
|
-
const userInvitationAccepted = WorkspacesEvents.UserInvitationAccepted({ email, createdBy: userId })
|
|
77
|
-
await Promise.all([
|
|
78
|
-
Aggregate.Of(User, signUpEvent),
|
|
79
|
-
Aggregate.Of(Token, signInEvent),
|
|
80
|
-
Aggregate.Of(Workspace, resolvedWorkspaceId).then(Aggregate.Add(userInvitationAccepted)),
|
|
81
|
-
])
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return { authToken: signInToken, authExpiresAt: signInExpiresAt }
|
|
85
|
-
}
|
|
1
|
+
export const metadata = { id: 'workspaces/accept-invitation', access: 'public' }
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken'
|
|
2
|
+
import { Aggregate } from '@ossy/event-store'
|
|
3
|
+
import { Workspace, WorkspacesEvents } from '@ossy/workspaces/server'
|
|
4
|
+
import { User, UsersEvents } from '@ossy/users/server'
|
|
5
|
+
import { Token, TokenEvents } from '@ossy/tokens/server'
|
|
6
|
+
import { ConfigService } from '@ossy/platform'
|
|
7
|
+
import { createLogger } from '@ossy/observability'
|
|
8
|
+
|
|
9
|
+
export const metadata = { id: 'workspaces/accept-invitation' }
|
|
10
|
+
|
|
11
|
+
const log = createLogger('workspaces/accept-invitation')
|
|
12
|
+
|
|
13
|
+
function verifyToken(token) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
if (!token) return reject(new Error('No token'))
|
|
16
|
+
jwt.verify(token, ConfigService.TokenSecret, { algorithms: ['HS256'] }, (err, payload) => {
|
|
17
|
+
if (err) return reject(err)
|
|
18
|
+
resolve(payload)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function run({ payload, req }) {
|
|
24
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
25
|
+
const verificationToken = payload?.token ?? req?.query?.token
|
|
26
|
+
|
|
27
|
+
const { aud: tokenWorkspaceId, sub: email } = await verifyToken(verificationToken)
|
|
28
|
+
.catch(() => { throw Object.assign(new Error('Invalid token'), { status: 401 }) })
|
|
29
|
+
|
|
30
|
+
const resolvedWorkspaceId = tokenWorkspaceId ?? workspaceId
|
|
31
|
+
const workspace = await Aggregate.Of(Workspace, resolvedWorkspaceId).then(Aggregate.View())
|
|
32
|
+
|
|
33
|
+
if (!email) throw Object.assign(new Error('No email found in token payload'), { status: 401 })
|
|
34
|
+
|
|
35
|
+
const existingUser = await Aggregate.Collection.findOne({ type: 'User', 'state.email': email }).then(agg => agg?.state)
|
|
36
|
+
|
|
37
|
+
if (existingUser && workspace.users.includes(existingUser.id)) {
|
|
38
|
+
throw Object.assign(new Error('User already in workspace'), { status: 400 })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let userId
|
|
42
|
+
let signInToken
|
|
43
|
+
let signInExpiresAt
|
|
44
|
+
|
|
45
|
+
if (existingUser) {
|
|
46
|
+
userId = existingUser.id
|
|
47
|
+
const signInExpiresIn = ConfigService.TokenValidity
|
|
48
|
+
signInExpiresAt = Date.now() + signInExpiresIn * 1000
|
|
49
|
+
signInToken = jwt.sign({ sub: userId, type: 'WebAuth' }, ConfigService.TokenSecret, { expiresIn: signInExpiresIn })
|
|
50
|
+
const signInEvent = TokenEvents.Created({
|
|
51
|
+
type: 'WebAuth',
|
|
52
|
+
subject: userId,
|
|
53
|
+
createdBy: userId,
|
|
54
|
+
token: signInToken,
|
|
55
|
+
expiresAt: signInExpiresAt,
|
|
56
|
+
})
|
|
57
|
+
const userInvitationAccepted = WorkspacesEvents.UserInvitationAccepted({ email, createdBy: userId })
|
|
58
|
+
await Promise.all([
|
|
59
|
+
Aggregate.Of(Token, signInEvent),
|
|
60
|
+
Aggregate.Of(Workspace, resolvedWorkspaceId).then(Aggregate.Add(userInvitationAccepted)),
|
|
61
|
+
])
|
|
62
|
+
} else {
|
|
63
|
+
const signUpEvent = UsersEvents.SignedUp({ email })
|
|
64
|
+
userId = signUpEvent.createdBy
|
|
65
|
+
const signInExpiresIn = ConfigService.TokenValidity
|
|
66
|
+
signInExpiresAt = Date.now() + signInExpiresIn * 1000
|
|
67
|
+
signInToken = jwt.sign({ sub: userId, type: 'WebAuth' }, ConfigService.TokenSecret, { expiresIn: signInExpiresIn })
|
|
68
|
+
const signInEvent = TokenEvents.Created({
|
|
69
|
+
type: 'WebAuth',
|
|
70
|
+
subject: userId,
|
|
71
|
+
createdBy: userId,
|
|
72
|
+
token: signInToken,
|
|
73
|
+
expiresAt: signInExpiresAt,
|
|
74
|
+
})
|
|
75
|
+
const userInvitationAccepted = WorkspacesEvents.UserInvitationAccepted({ email, createdBy: userId })
|
|
76
|
+
await Promise.all([
|
|
77
|
+
Aggregate.Of(User, signUpEvent),
|
|
78
|
+
Aggregate.Of(Token, signInEvent),
|
|
79
|
+
Aggregate.Of(Workspace, resolvedWorkspaceId).then(Aggregate.Add(userInvitationAccepted)),
|
|
80
|
+
])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { authToken: signInToken, authExpiresAt: signInExpiresAt }
|
|
84
|
+
}
|
|
@@ -1,17 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Workspace, WorkspacesEvents } from '@ossy/workspaces'
|
|
3
|
-
|
|
4
|
-
export const id = 'workspaces/create-api-token'
|
|
5
|
-
export const access = 'workspace'
|
|
6
|
-
|
|
7
|
-
export async function run({ payload, req }) {
|
|
8
|
-
const createdBy = payload?.userId ?? req?.userId
|
|
9
|
-
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
10
|
-
const description = payload?.description
|
|
11
|
-
|
|
12
|
-
const event = WorkspacesEvents.ApiTokenCreated({ createdBy, workspaceId, description })
|
|
13
|
-
|
|
14
|
-
const aggregate = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.Add(event))
|
|
15
|
-
const last = aggregate.events[aggregate.events.length - 1]
|
|
16
|
-
return { id: last.id, token: event.payload.token }
|
|
17
|
-
}
|
|
1
|
+
export const metadata = { id: 'workspaces/create-api-token', access: 'workspace' }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Workspace, WorkspacesEvents } from '@ossy/workspaces/server'
|
|
3
|
+
|
|
4
|
+
export const metadata = { id: 'workspaces/create-api-token' }
|
|
5
|
+
|
|
6
|
+
export async function run({ payload, req }) {
|
|
7
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
8
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
9
|
+
const description = payload?.description
|
|
10
|
+
|
|
11
|
+
const event = WorkspacesEvents.ApiTokenCreated({ createdBy, workspaceId, description })
|
|
12
|
+
|
|
13
|
+
const aggregate = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.Add(event))
|
|
14
|
+
const last = aggregate.events[aggregate.events.length - 1]
|
|
15
|
+
return { id: last.id, token: event.payload.token }
|
|
16
|
+
}
|
package/src/create.action.js
CHANGED
|
@@ -1,30 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Workspace, WorkspacesEvents } from '@ossy/workspaces'
|
|
3
|
-
import { ConfigService } from '@ossy/platform'
|
|
4
|
-
|
|
5
|
-
export const id = 'workspaces/create'
|
|
6
|
-
export const access = 'authenticated'
|
|
7
|
-
|
|
8
|
-
export async function run({ payload, req }) {
|
|
9
|
-
const createdBy = payload?.userId ?? req?.userId
|
|
10
|
-
const name = (payload?.name ?? req?.body?.name)?.trim?.()
|
|
11
|
-
|
|
12
|
-
if (!name || typeof name !== 'string') {
|
|
13
|
-
throw Object.assign(new Error('Invalid name'), { status: 400 })
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const createdEvent = WorkspacesEvents.Created({ createdBy, name })
|
|
17
|
-
const botUserInvitedEvent = WorkspacesEvents.UserInvited({
|
|
18
|
-
createdBy: ConfigService.BotUserId,
|
|
19
|
-
email: ConfigService.BotUserEmail,
|
|
20
|
-
})
|
|
21
|
-
const botUserAcceptedEvent = WorkspacesEvents.UserInvitationAccepted({
|
|
22
|
-
createdBy: ConfigService.BotUserId,
|
|
23
|
-
email: ConfigService.BotUserEmail,
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
return Aggregate.Of(Workspace, createdEvent)
|
|
27
|
-
.then(Aggregate.Add(botUserInvitedEvent))
|
|
28
|
-
.then(Aggregate.Add(botUserAcceptedEvent))
|
|
29
|
-
.then(Aggregate.View())
|
|
30
|
-
}
|
|
1
|
+
export const metadata = { id: 'workspaces/create', access: 'authenticated' }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Workspace, WorkspacesEvents } from '@ossy/workspaces/server'
|
|
3
|
+
import { ConfigService } from '@ossy/platform'
|
|
4
|
+
|
|
5
|
+
export const metadata = { id: 'workspaces/create' }
|
|
6
|
+
|
|
7
|
+
export async function run({ payload, req }) {
|
|
8
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
9
|
+
const name = (payload?.name ?? req?.body?.name)?.trim?.()
|
|
10
|
+
|
|
11
|
+
if (!name || typeof name !== 'string') {
|
|
12
|
+
throw Object.assign(new Error('Invalid name'), { status: 400 })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const createdEvent = WorkspacesEvents.Created({ createdBy, name })
|
|
16
|
+
const botUserInvitedEvent = WorkspacesEvents.UserInvited({
|
|
17
|
+
createdBy: ConfigService.BotUserId,
|
|
18
|
+
email: ConfigService.BotUserEmail,
|
|
19
|
+
})
|
|
20
|
+
const botUserAcceptedEvent = WorkspacesEvents.UserInvitationAccepted({
|
|
21
|
+
createdBy: ConfigService.BotUserId,
|
|
22
|
+
email: ConfigService.BotUserEmail,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return Aggregate.Of(Workspace, createdEvent)
|
|
26
|
+
.then(Aggregate.Add(botUserInvitedEvent))
|
|
27
|
+
.then(Aggregate.Add(botUserAcceptedEvent))
|
|
28
|
+
.then(Aggregate.View())
|
|
29
|
+
}
|
|
@@ -1,28 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Workspace, WorkspacesEvents } from '@ossy/workspaces'
|
|
3
|
-
|
|
4
|
-
export const id = 'workspaces/disable-service'
|
|
5
|
-
export const access = 'workspace'
|
|
6
|
-
|
|
7
|
-
const AVAILABLE_SERVICES = [
|
|
8
|
-
'@ossy/tasks/visual-content-descriptors',
|
|
9
|
-
'@ossy/tasks/resize-common-web',
|
|
10
|
-
'@ossy/resumes',
|
|
11
|
-
'@ossy/consultancy',
|
|
12
|
-
'@ossy/apps',
|
|
13
|
-
'@ossy/domains',
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
export async function run({ payload, req }) {
|
|
17
|
-
const createdBy = payload?.userId ?? req?.userId
|
|
18
|
-
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
19
|
-
const service = payload?.service
|
|
20
|
-
|
|
21
|
-
if (!AVAILABLE_SERVICES.includes(service)) {
|
|
22
|
-
throw Object.assign(new Error(`Service ${service} is not available`), { status: 400 })
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const event = WorkspacesEvents.ServiceDisabled({ createdBy, service })
|
|
26
|
-
await Aggregate.Of(Workspace, workspaceId).then(Aggregate.Add(event))
|
|
27
|
-
return null
|
|
28
|
-
}
|
|
1
|
+
export const metadata = { id: 'workspaces/disable-service', access: 'workspace' }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Workspace, WorkspacesEvents } from '@ossy/workspaces/server'
|
|
3
|
+
|
|
4
|
+
export const metadata = { id: 'workspaces/disable-service' }
|
|
5
|
+
|
|
6
|
+
const AVAILABLE_SERVICES = [
|
|
7
|
+
'@ossy/tasks/visual-content-descriptors',
|
|
8
|
+
'@ossy/tasks/resize-common-web',
|
|
9
|
+
'@ossy/resumes',
|
|
10
|
+
'@ossy/consultancy',
|
|
11
|
+
'@ossy/apps',
|
|
12
|
+
'@ossy/domains',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
export async function run({ payload, req }) {
|
|
16
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
17
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
18
|
+
const service = payload?.service
|
|
19
|
+
|
|
20
|
+
if (!AVAILABLE_SERVICES.includes(service)) {
|
|
21
|
+
throw Object.assign(new Error(`Service ${service} is not available`), { status: 400 })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const event = WorkspacesEvents.ServiceDisabled({ createdBy, service })
|
|
25
|
+
await Aggregate.Of(Workspace, workspaceId).then(Aggregate.Add(event))
|
|
26
|
+
return null
|
|
27
|
+
}
|
package/src/en.translations.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"workspaces
|
|
2
|
+
"profile/workspaces/create.documentTitle": "Create workspace",
|
|
3
3
|
"workspaces.create.title": "Create a new workspace",
|
|
4
4
|
"workspaces.create.namePlaceholder": "Workspace name",
|
|
5
5
|
"workspaces.create.cancel": "Cancel",
|
|
6
6
|
"workspaces.create.submit": "Create",
|
|
7
|
-
"workspaces.
|
|
7
|
+
"profile/workspaces.documentTitle": "Select workspace",
|
|
8
8
|
"workspaces.select.title": "My workspaces",
|
|
9
9
|
"workspaces.select.newWorkspace": "New workspace",
|
|
10
10
|
"workspaces.select.loading": "Loading...",
|
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
"workspaces/remove-member.description": "Remove a user from the workspace",
|
|
44
44
|
"workspaces/list.label": "List workspaces",
|
|
45
45
|
"workspaces/list.description": "List workspaces for the authenticated user",
|
|
46
|
-
"workspace
|
|
46
|
+
"workspace/settings.documentTitle": "Workspace settings",
|
|
47
47
|
"workspace.settings.title": "Workspace settings",
|
|
48
|
-
"workspace
|
|
48
|
+
"workspace/users.documentTitle": "Users",
|
|
49
49
|
"workspace.users.title": "Users",
|
|
50
50
|
"workspace.users.description": "Manage users in your workspace.",
|
|
51
51
|
"workspace.users.invite": "Invite user",
|
|
52
52
|
"workspace.users.loading": "Loading",
|
|
53
|
-
"workspace
|
|
54
|
-
"workspace
|
|
53
|
+
"workspace/invitations.documentTitle": "Invitations",
|
|
54
|
+
"workspace/invitations/add.documentTitle": "Invite user",
|
|
55
55
|
"workspace.invitations.title": "Invitations",
|
|
56
56
|
"workspace.settings.detailsTitle": "Workspace details",
|
|
57
57
|
"workspace.settings.edit": "Edit",
|
|
@@ -1,28 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Workspace, WorkspacesEvents } from '@ossy/workspaces'
|
|
3
|
-
|
|
4
|
-
export const id = 'workspaces/enable-service'
|
|
5
|
-
export const access = 'workspace'
|
|
6
|
-
|
|
7
|
-
const AVAILABLE_SERVICES = [
|
|
8
|
-
'@ossy/tasks/visual-content-descriptors',
|
|
9
|
-
'@ossy/tasks/resize-common-web',
|
|
10
|
-
'@ossy/resumes',
|
|
11
|
-
'@ossy/consultancy',
|
|
12
|
-
'@ossy/apps',
|
|
13
|
-
'@ossy/domains',
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
export async function run({ payload, req }) {
|
|
17
|
-
const createdBy = payload?.userId ?? req?.userId
|
|
18
|
-
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
19
|
-
const service = payload?.service ?? payload
|
|
20
|
-
|
|
21
|
-
if (!AVAILABLE_SERVICES.includes(service)) {
|
|
22
|
-
throw Object.assign(new Error(`Service ${service} is not available`), { status: 400 })
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const event = WorkspacesEvents.ServiceEnabled({ createdBy, service })
|
|
26
|
-
await Aggregate.Of(Workspace, workspaceId).then(Aggregate.Add(event))
|
|
27
|
-
return null
|
|
28
|
-
}
|
|
1
|
+
export const metadata = { id: 'workspaces/enable-service', access: 'workspace' }
|