Skip to content

Commit 513778a

Browse files
committed
Merge branch 'feature/delete-pod' of github.com:solid/node-solid-server into feature/delete-pod
2 parents 235523e + f7c66f0 commit 513778a

9 files changed

+587
-32
lines changed

lib/models/account-manager.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,40 @@ class AccountManager {
418418
* @return {string} Generated token
419419
*/
420420
generateResetToken (userAccount) {
421-
return this.tokenService.generate({ webId: userAccount.webId })
421+
return this.tokenService.generate('reset-password', { webId: userAccount.webId })
422+
}
423+
424+
/**
425+
* Generates an expiring one-time-use token for password reset purposes
426+
* (the user's Web ID is saved in the token service).
427+
*
428+
* @param userAccount {UserAccount}
429+
*
430+
* @return {string} Generated token
431+
*/
432+
generateDeleteToken (userAccount) {
433+
return this.tokenService.generate('delete-account', { webId: userAccount.webId })
434+
}
435+
436+
/**
437+
* Validates that a token exists and is not expired, and returns the saved
438+
* token contents, or throws an error if invalid.
439+
* Does not consume / clear the token.
440+
*
441+
* @param token {string}
442+
*
443+
* @throws {Error} If missing or invalid token
444+
*
445+
* @return {Object|false} Saved token data object if verified, false otherwise
446+
*/
447+
validateDeleteToken (token) {
448+
let tokenValue = this.tokenService.verify('delete-account', token)
449+
450+
if (!tokenValue) {
451+
throw new Error('Invalid or expired delete account token')
452+
}
453+
454+
return tokenValue
422455
}
423456

424457
/**
@@ -433,7 +466,7 @@ class AccountManager {
433466
* @return {Object|false} Saved token data object if verified, false otherwise
434467
*/
435468
validateResetToken (token) {
436-
let tokenValue = this.tokenService.verify(token)
469+
let tokenValue = this.tokenService.verify('reset-password', token)
437470

438471
if (!tokenValue) {
439472
throw new Error('Invalid or expired reset token')
@@ -515,7 +548,7 @@ class AccountManager {
515548
sendDeleteAccountEmail (userAccount) {
516549
return Promise.resolve()
517550
.then(() => this.verifyEmailDependencies(userAccount))
518-
.then(() => this.generateResetToken(userAccount))
551+
.then(() => this.generateDeleteToken(userAccount))
519552
.then(resetToken => {
520553
const deleteUrl = this.getAccountDeleteUrl(resetToken)
521554

lib/models/token-service.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,26 @@ class TokenService {
77
this.tokens = {}
88
}
99

10-
generate (data = {}) {
10+
generate (domain, data = {}) {
1111
const token = ulid()
12+
this.tokens[domain] = this.tokens[domain] || {}
1213

1314
const value = {
1415
exp: new Date(Date.now() + 20 * 60 * 1000)
1516
}
16-
17-
this.tokens[token] = Object.assign({}, value, data)
17+
this.tokens[domain][token] = Object.assign({}, value, data)
1818

1919
return token
2020
}
2121

22-
verify (token) {
22+
verify (domain, token) {
2323
const now = new Date()
2424

25-
let tokenValue = this.tokens[token]
25+
if (!this.tokens[domain]) {
26+
throw new Error(`Invalid domain for tokens: ${domain}`)
27+
}
28+
29+
let tokenValue = this.tokens[domain][token]
2630

2731
if (tokenValue && now < tokenValue.exp) {
2832
return tokenValue
@@ -31,8 +35,12 @@ class TokenService {
3135
}
3236
}
3337

34-
remove (token) {
35-
delete this.tokens[token]
38+
remove (domain, token) {
39+
if (!this.tokens[domain]) {
40+
throw new Error(`Invalid domain for tokens: ${domain}`)
41+
}
42+
43+
delete this.tokens[domain][token]
3644
}
3745
}
3846

lib/requests/delete-account-confirm-request.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class DeleteAccountConfirmRequest extends AuthRequest {
106106
.then(() => {
107107
if (!this.token) { return false }
108108

109-
return this.accountManager.validateResetToken(this.token)
109+
return this.accountManager.validateDeleteToken(this.token)
110110
})
111111
.then(validToken => {
112112
if (validToken) {

lib/requests/delete-account-request.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,10 @@ class DeleteAccountRequest extends AuthRequest {
4949
* Renders the Delete form
5050
*/
5151
renderForm (error) {
52-
const params = {
52+
this.response.render('account/delete', {
5353
error,
5454
multiuser: this.accountManager.multiuser
55-
}
56-
57-
this.response.render('account/delete', params)
55+
})
5856
}
5957

6058
/**
@@ -102,6 +100,18 @@ class DeleteAccountRequest extends AuthRequest {
102100

103101
debug(`User '${request.username}' requested to be sent a delete account email`)
104102

103+
return DeleteAccountRequest.handlePost(request)
104+
}
105+
106+
/**
107+
* Performs a 'send me a password reset email' request operation, after the
108+
* user has entered an email into the reset form.
109+
*
110+
* @param request {DeleteAccountRequest}
111+
*
112+
* @return {Promise}
113+
*/
114+
static handlePost (request) {
105115
return Promise.resolve()
106116
.then(() => request.validate())
107117
.then(() => request.loadUser())
@@ -111,11 +121,9 @@ class DeleteAccountRequest extends AuthRequest {
111121
}
112122

113123
static get (req, res) {
114-
let request = DeleteAccountRequest.fromParams(req, res)
124+
const request = DeleteAccountRequest.fromParams(req, res)
115125

116-
return Promise.resolve()
117-
.then(() => request.renderForm())
118-
.catch(error => request.error(error))
126+
request.renderForm()
119127
}
120128

121129
static fromParams (req, res) {

lib/requests/password-reset-email-request.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class PasswordResetEmailRequest extends AuthRequest {
8585
* Performs a 'send me a password reset email' request operation, after the
8686
* user has entered an email into the reset form.
8787
*
88-
* @param request {IncomingRequest}
88+
* @param request {PasswordResetEmailRequest}
8989
*
9090
* @return {Promise}
9191
*/

test/unit/account-manager-test.js

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,27 @@ describe('AccountManager', () => {
390390
})
391391
})
392392

393+
describe('generateDeleteToken()', () => {
394+
it('should generate and store an expiring delete token', () => {
395+
let tokenService = new TokenService()
396+
let options = { host, tokenService }
397+
398+
let accountManager = AccountManager.from(options)
399+
400+
let aliceWebId = 'https://alice.example.com/#me'
401+
let userAccount = {
402+
webId: aliceWebId
403+
}
404+
405+
let token = accountManager.generateDeleteToken(userAccount)
406+
407+
let tokenValue = accountManager.tokenService.verify('delete-account', token)
408+
409+
expect(tokenValue.webId).to.equal(aliceWebId)
410+
expect(tokenValue).to.have.property('exp')
411+
})
412+
})
413+
393414
describe('generateResetToken()', () => {
394415
it('should generate and store an expiring reset token', () => {
395416
let tokenService = new TokenService()
@@ -404,7 +425,7 @@ describe('AccountManager', () => {
404425

405426
let token = accountManager.generateResetToken(userAccount)
406427

407-
let tokenValue = accountManager.tokenService.verify(token)
428+
let tokenValue = accountManager.tokenService.verify('reset-password', token)
408429

409430
expect(tokenValue.webId).to.equal(aliceWebId)
410431
expect(tokenValue).to.have.property('exp')
@@ -484,4 +505,75 @@ describe('AccountManager', () => {
484505
})
485506
})
486507
})
508+
509+
describe('sendDeleteAccountEmail()', () => {
510+
it('should compose and send a delete account email', () => {
511+
let deleteToken = '1234'
512+
let tokenService = {
513+
generate: sinon.stub().returns(deleteToken)
514+
}
515+
516+
let emailService = {
517+
sendWithTemplate: sinon.stub().resolves()
518+
}
519+
520+
let aliceWebId = 'https://alice.example.com/#me'
521+
let userAccount = {
522+
webId: aliceWebId,
523+
524+
}
525+
526+
let options = { host, tokenService, emailService }
527+
let accountManager = AccountManager.from(options)
528+
529+
accountManager.getAccountDeleteUrl = sinon.stub().returns('delete account url')
530+
531+
let expectedEmailData = {
532+
533+
webId: aliceWebId,
534+
deleteUrl: 'delete account url'
535+
}
536+
537+
return accountManager.sendDeleteAccountEmail(userAccount)
538+
.then(() => {
539+
expect(accountManager.getAccountDeleteUrl)
540+
.to.have.been.calledWith(deleteToken)
541+
expect(emailService.sendWithTemplate)
542+
.to.have.been.calledWith('delete-account', expectedEmailData)
543+
})
544+
})
545+
546+
it('should reject if no email service is set up', done => {
547+
let aliceWebId = 'https://alice.example.com/#me'
548+
let userAccount = {
549+
webId: aliceWebId,
550+
551+
}
552+
let options = { host }
553+
let accountManager = AccountManager.from(options)
554+
555+
accountManager.sendDeleteAccountEmail(userAccount)
556+
.catch(error => {
557+
expect(error.message).to.equal('Email service is not set up')
558+
done()
559+
})
560+
})
561+
562+
it('should reject if no user email is provided', done => {
563+
let aliceWebId = 'https://alice.example.com/#me'
564+
let userAccount = {
565+
webId: aliceWebId
566+
}
567+
let emailService = {}
568+
let options = { host, emailService }
569+
570+
let accountManager = AccountManager.from(options)
571+
572+
accountManager.sendDeleteAccountEmail(userAccount)
573+
.catch(error => {
574+
expect(error.message).to.equal('Account recovery email has not been provided')
575+
done()
576+
})
577+
})
578+
})
487579
})

0 commit comments

Comments
 (0)