Skip to content
This repository has been archived by the owner on Mar 31, 2021. It is now read-only.

Commit

Permalink
allow replacing subscription, fixes #54
Browse files Browse the repository at this point in the history
  • Loading branch information
f-w committed Aug 22, 2019
1 parent cc7a2ea commit 6d7f608
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 30 deletions.
31 changes: 29 additions & 2 deletions common/models/subscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,11 @@ module.exports = function(Subscription) {
}
}

Subscription.prototype.verify = async function(options, confirmationCode) {
Subscription.prototype.verify = async function(
options,
confirmationCode,
replace
) {
let mergedSubscriptionConfig = await Subscription.getMergedConfig(
'subscription',
this.serviceName
Expand Down Expand Up @@ -581,8 +585,31 @@ module.exports = function(Subscription) {
error.status = 403
return await handleConfirmationAcknowledgement(error)
}
this.state = 'confirmed'
try {
if (replace && this.userChannelId) {
let whereClause = {
serviceName: this.serviceName,
state: 'confirmed',
channel: this.channel
}
// email address check should be case insensitive
let escapedUserChannelId = this.userChannelId.replace(
/[-[\]{}()*+?.,\\^$|#\s]/g,
'\\$&'
)
let escapedUserChannelIdRegExp = new RegExp(escapedUserChannelId, 'i')
whereClause.userChannelId = {
regexp: escapedUserChannelIdRegExp
}
await Subscription.updateAll(
whereClause,
{
state: 'deleted'
},
options
)
}
this.state = 'confirmed'
await Subscription.replaceById(this.id, this, options)
} catch (err) {
return await handleConfirmationAcknowledgement(err)
Expand Down
15 changes: 11 additions & 4 deletions common/models/subscription.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@
"http": {
"source": "query"
}
},
{
"arg": "replace",
"type": "boolean",
"required": false,
"description": "whether or not replacing existing subscriptions",
"http": {
"source": "query"
}
}
],
"returns": [
Expand Down Expand Up @@ -202,9 +211,7 @@
"returns": [
{
"arg": "result",
"type": [
"string"
],
"type": ["string"],
"root": true,
"description": "operation outcome"
}
Expand All @@ -218,4 +225,4 @@
]
}
}
}
}
12 changes: 9 additions & 3 deletions docs/_docs/api-subscription.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,16 +390,22 @@ GET /subscriptions/{id}/verify
* required: true
* parameter type: query
* data type: string
* whether or not replacing existing subscriptions
* parameter name: replace
* required: false
* parameter type: query
* data type: boolean
* outcome
*NotifyBC* performs following actions in sequence
1. the subscription identified by *id* is retrieved
2. for user request, the *userId* of the subscription is checked against current request user, if not match, error is returned; otherwise
3. input parameter *confirmationCode* is checked against *confirmationRequest.confirmationCode*. If not match, error is returned; otherwise
4. *state* is set to *confirmed*
5. the subscription is saved back to database
6. displays acknowledgement message according to [configuration](../config-subscription#confirmation-verification-acknowledgement-messages)
4. if input parameter *replace* is supplied and set to *true*, then existing confirmed subscriptions from the same *serviceName*, *channel* and *userChannelId* are deleted. No unsubscription acknowledgement notification is sent
5. *state* is set to *confirmed*
6. the subscription is saved back to database
7. displays acknowledgement message according to [configuration](../config-subscription#confirmation-verification-acknowledgement-messages)
## Update a Subscription
```
Expand Down
20 changes: 19 additions & 1 deletion docs/_docs/config-subscription.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ If error happened during subscription confirmation, query string *?err=\<error\>

## Duplicated Subscription
*NotifyBC* by default allows a user subscribe to a service through same channel multiple times. If this is undesirable, you can set config *subscription.detectDuplicatedSubscription* to true. In such case instead of sending user a confirmation request, *NotifyBC* sends user a duplicated subscription notification message. Unlike a confirmation request, duplicated subscription
notification message doesn't and shouldn't contain any information to allow user confirm the subscription. You can customize duplicated subscription notification message by setting config *subscription.duplicatedSubscriptionNotification* in either *config.local.js* or using configuration api for service-specific dynamic config. Following is the default settings defined in
notification message either shouldn't contain any information to allow user confirm the subscription, or it should contain a link that allows user to replace existing confirmed subscription with this one. You can customize duplicated subscription notification message by setting config *subscription.duplicatedSubscriptionNotification* in either *config.local.js* or using configuration api for service-specific dynamic config. Following is the default settings defined in
*config.json*

```json
Expand All @@ -119,6 +119,24 @@ notification message doesn't and shouldn't contain any information to allow user
}
```

To allow user to replace existing confirmed subscription, set the message to something like

```json
{
...
"subscription": {
...
"detectDuplicatedSubscription": false,
"duplicatedSubscriptionNotification": {
"email": {
"textBody": "A duplicated subscription was submitted. If the request is not submitted by you, please ignore this message. Otherwise if you want to replace existing subscription with this one, click {subscription_confirmation_url}&replace=true."
}
}
}
}
```
The query parameter *&replace=true* following the token *{subscription_confirmation_url}* will cause existing subscription be replaced.

## Anonymous Unsubscription
For anonymous subscription, *NotifyBC* supports one-click opt-out by allowing unsubscription URL provided in notifications. To thwart unauthorized unsubscription attempts, *NotifyBC* implemented and enabled by default two security measurements

Expand Down
51 changes: 31 additions & 20 deletions spec/app/subscription.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -476,11 +476,6 @@ describe('GET /subscriptions/{id}/verify', function() {
userChannelId: '[email protected]',
state: 'unconfirmed',
confirmationRequest: {
confirmationCodeRegex: '\\d{5}',
sendRequest: true,
from: '[email protected]',
subject: 'Subscription confirmation',
textBody: 'enter {confirmation_code} in this email',
confirmationCode: '37688'
}
},
Expand All @@ -497,14 +492,21 @@ describe('GET /subscriptions/{id}/verify', function() {
userChannelId: '[email protected]',
state: 'unconfirmed',
confirmationRequest: {
confirmationCodeRegex: '\\d{5}',
sendRequest: true,
from: '[email protected]',
subject: 'Subscription confirmation',
textBody: 'enter {confirmation_code} in this email',
confirmationCode: '37689'
},
unsubscriptionCode: '50032'
}
},
function(err, res) {
cb(err, res)
}
)
},
function(cb) {
app.models.Subscription.create(
{
serviceName: 'myService',
channel: 'email',
userChannelId: '[email protected]',
state: 'confirmed'
},
function(err, res) {
cb(err, res)
Expand All @@ -527,24 +529,33 @@ describe('GET /subscriptions/{id}/verify', function() {
})

it('should verify confirmation code sent by anonymous user', async function() {
let res = await request(app)
.get(
'/api/subscriptions/' + data[1].id + '/verify?confirmationCode=37689'
)
.set('Accept', 'application/json')
let res = await request(app).get(
'/api/subscriptions/' + data[1].id + '/verify?confirmationCode=37689'
)
expect(res.statusCode).toBe(200)
res = await app.models.Subscription.findById(data[1].id)
expect(res.state).toBe('confirmed')
})

it('should deny incorrect confirmation code', async function() {
let res = await request(app)
.get('/api/subscriptions/' + data[1].id + '/verify?confirmationCode=0000')
.set('Accept', 'application/json')
let res = await request(app).get(
'/api/subscriptions/' + data[1].id + '/verify?confirmationCode=0000'
)
expect(res.statusCode).toBe(403)
res = await app.models.Subscription.findById(data[1].id)
expect(res.state).toBe('unconfirmed')
})

it('should unsubscribe existing subscriptions when replace paramter is supplied', async function() {
let res = await request(app).get(
'/api/subscriptions/' +
data[1].id +
'/verify?confirmationCode=37689&replace=true'
)
expect(res.statusCode).toBe(200)
res = await app.models.Subscription.findById(data[2].id)
expect(res.state).toBe('deleted')
})
})

describe('DELETE /subscriptions/{id}', function() {
Expand Down

0 comments on commit 6d7f608

Please sign in to comment.