Skip to content

Commit 2b56455

Browse files
committed
Implement sending email notifications via POST /notification
1 parent ed09e46 commit 2b56455

File tree

8 files changed

+254
-153
lines changed

8 files changed

+254
-153
lines changed

src/app.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import {
1010
finishIntegration,
1111
initializeIntegration,
1212
} from './controllers/integration'
13+
import { notification } from './controllers/notification'
1314
import { getStatus } from './controllers/status'
14-
import { webhookReceiver } from './controllers/webhookReceiver'
1515
import {
1616
authorizeGroups,
17-
checkParamGroupMembership,
17+
checkGroupMembership,
1818
} from './middlewares/authorizeGroup'
1919
import { solidAuth } from './middlewares/solidAuth'
2020
import { validateBody } from './middlewares/validate'
@@ -53,12 +53,55 @@ router
5353
initializeIntegration,
5454
)
5555
.get('/verify-email', checkVerificationLink, finishIntegration)
56-
.post('/webhook-receiver', webhookReceiver)
56+
.post(
57+
'/notification',
58+
solidAuth,
59+
authorizeGroups(allowedGroups),
60+
validateBody({
61+
type: 'object',
62+
properties: {
63+
'@context': { const: 'https://www.w3.org/ns/activitystreams' },
64+
id: { type: 'string' },
65+
type: { const: 'Create' },
66+
actor: {
67+
type: 'object',
68+
properties: {
69+
type: { const: 'Person' },
70+
id: { type: 'string', format: 'uri' },
71+
name: { type: 'string' },
72+
},
73+
required: ['type', 'id'],
74+
},
75+
object: {
76+
type: 'object',
77+
properties: {
78+
type: { const: 'Note' },
79+
id: { type: 'string', format: 'uri' },
80+
content: { type: 'string' },
81+
},
82+
required: ['type', 'id', 'content'],
83+
},
84+
target: {
85+
type: 'object',
86+
properties: {
87+
type: { const: 'Person' },
88+
id: { type: 'string', format: 'uri' },
89+
name: { type: 'string' },
90+
},
91+
required: ['type', 'id'],
92+
},
93+
},
94+
required: ['@context', 'type', 'actor', 'object', 'target'],
95+
additionalProperties: false,
96+
}),
97+
checkGroupMembership(allowedGroups, 'request.body.target.id', 400),
98+
notification,
99+
)
57100
.get(
58101
'/status/:webId',
59102
solidAuth,
60103
authorizeGroups(allowedGroups),
61-
checkParamGroupMembership(allowedGroups, 'webId' as const),
104+
checkGroupMembership(allowedGroups, 'params.webId', 400),
62105
getStatus,
63106
)
64107

src/controllers/notification.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { DefaultContext, Middleware } from 'koa'
2+
import { emailSender } from '../config'
3+
import { sendMail } from '../services/mailerService'
4+
import { getVerifiedEmails } from './status'
5+
6+
export type GoodBody = {
7+
'@context': 'https://www.w3.org/ns/activitystreams'
8+
type: 'Create'
9+
actor: { type: 'Person'; id: string; name?: string }
10+
object: { type: 'Note'; id: string; content: string }
11+
target: { type: 'Person'; id: string; name?: string }
12+
}
13+
14+
export const notification: Middleware<
15+
{ user: string; client: string | undefined },
16+
DefaultContext & { request: { body: GoodBody } }
17+
> = async ctx => {
18+
const body: GoodBody = ctx.request.body
19+
20+
if (ctx.state.user !== body.actor.id)
21+
return ctx.throw(403, "You can't send notification as somebody else")
22+
23+
// find email address
24+
const emails = await getVerifiedEmails(body.target.id)
25+
26+
if (emails.length === 0)
27+
return ctx.throw(
28+
404,
29+
"Receiving person doesn't have available email address",
30+
)
31+
32+
for (const email of emails) {
33+
await sendMail({
34+
from: emailSender,
35+
to: email,
36+
subject: 'You have a new message from sleepy.bike!', // TODO generalize
37+
html: body.object.content,
38+
text: body.object.content,
39+
})
40+
}
41+
ctx.response.status = 202
42+
ctx.response.body = 'Accepted'
43+
}

src/controllers/webhookReceiver.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

src/middlewares/authorizeGroup.ts

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,17 @@
1-
import { DefaultContext, DefaultState, Middleware } from 'koa'
1+
import { Middleware } from 'koa'
2+
import { get } from 'lodash'
23
import { Parser } from 'n3'
34
import { vcard } from 'rdf-namespaces'
45

5-
export const authorizeGroups =
6-
(groups: string[]): Middleware<{ user: string }> =>
7-
async (ctx, next) => {
8-
// if array of groups are empty, we allow everybody (default)
9-
if (groups.length === 0) return await next()
10-
11-
const user = ctx.state.user
12-
13-
const isAllowed = await isSomeGroupMember(user, groups)
14-
15-
if (!isAllowed) {
16-
return ctx.throw(
17-
403,
18-
'Authenticated user is not a member of any allowed group',
19-
)
20-
}
21-
22-
await next()
23-
}
6+
export const authorizeGroups = (
7+
groups: string[],
8+
): Middleware<{ user: string }> =>
9+
checkGroupMembership(
10+
groups,
11+
'state.user',
12+
403,
13+
'Authenticated user is not a member of any allowed group',
14+
)
2415

2516
const isSomeGroupMember = async (user: string, groups: string[]) => {
2617
const memberships = await Promise.allSettled(
@@ -37,29 +28,24 @@ const isSomeGroupMember = async (user: string, groups: string[]) => {
3728
/**
3829
* Check whether a user specified in param is member of any of the given groups
3930
*/
40-
export const checkParamGroupMembership =
41-
<T extends string>(
31+
export const checkGroupMembership =
32+
(
4233
groups: string[],
43-
param: T,
44-
): Middleware<
45-
DefaultState,
46-
DefaultContext & { params: { [K in T]: string } }
47-
> =>
34+
path: string,
35+
status: number,
36+
error = 'Person is not a member of any allowed group',
37+
): Middleware =>
4838
async (ctx, next) => {
4939
// if array of groups are empty, we allow everybody (default)
5040
if (groups.length === 0) return await next()
51-
const webId = ctx.params[param]
41+
const webId = get(ctx, path)
42+
if (typeof webId !== 'string')
43+
throw new Error('Expected string, got ' + typeof webId)
5244
const isAllowed = await isSomeGroupMember(webId, groups)
5345

5446
if (!isAllowed) {
55-
return ctx.throw(400, {
56-
error: 'Person is not a member of any allowed group',
57-
person: webId,
58-
groups,
59-
})
60-
}
61-
62-
await next()
47+
return ctx.throw(status, error)
48+
} else await next()
6349
}
6450

6551
const isGroupMember = async (user: string, group: string) => {

src/test/helpers/setupPod.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { expect } from 'chai'
22
import { foaf, ldp, solid, space } from 'rdf-namespaces'
33
import * as config from '../../config'
4-
import { authenticatedFetch } from '../testSetup.spec'
54
import { Person } from './types'
65

76
const createFile = async ({
@@ -17,9 +16,10 @@ const createFile = async ({
1716
}) => {
1817
const response = await authenticatedFetch(url, {
1918
method: 'PUT',
20-
body: body,
19+
body,
2120
headers: { 'content-type': 'text/turtle' },
2221
})
22+
2323
expect(response.ok).to.be.true
2424

2525
if (acl && acl.read) {
@@ -63,10 +63,12 @@ export const setupEmailSettings = async ({
6363
person,
6464
email,
6565
emailVerificationToken,
66+
authenticatedFetch,
6667
}: {
6768
person: Person
6869
email: string
6970
emailVerificationToken: string
71+
authenticatedFetch: typeof fetch
7072
}) => {
7173
// add email settings, readable by mailer
7274
const settings = `

0 commit comments

Comments
 (0)