Skip to content

Commit 6bfafe8

Browse files
committed
The basics for client and API
TODO: Change tokens a bit
1 parent b53cb85 commit 6bfafe8

File tree

8 files changed

+494
-8
lines changed

8 files changed

+494
-8
lines changed

default-templates/new-account/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ <h1>Apps</h1>
6464
</div>
6565
</div>
6666
</section>
67+
<section class="row">
68+
<div class="col-md-12">
69+
<h1>Settings</h1>
70+
<div class="list-group">
71+
<a href="/account/delete/" class="list-group-item">
72+
<span class="lead">Delete account</span>
73+
</a>
74+
</div>
75+
</div>
76+
</section>
6777
</div>
6878
<script src="/common/js/solid-auth-client.bundle.js"></script>
6979
<script type="text/javascript">
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Account Deleted</title>
7+
<link rel="stylesheet" href="/common/css/bootstrap.min.css">
8+
</head>
9+
<body>
10+
<div class="container">
11+
<h4>Account Deleted</h4>
12+
</div>
13+
<div class="container">
14+
<p>Your account has been deleted.</p>
15+
</div>
16+
</body>
17+
</html>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Delete Account</title>
7+
<link rel="stylesheet" href="/common/css/bootstrap.min.css">
8+
</head>
9+
<body>
10+
<div class="container">
11+
<h4>Delete Account</h4>
12+
</div>
13+
<div class="container">
14+
<form method="post" action="/account/delete/confirm">
15+
{{#if error}}
16+
<div class="form-group">
17+
<div class="row">
18+
<div class="col-md-12">
19+
<p class="text-danger"><strong>{{error}}</strong></p>
20+
</div>
21+
</div>
22+
</div>
23+
{{/if}}
24+
25+
{{#if validToken}}
26+
<p>Beware that this is an irreversible action. All your data that is stored in the POD will be deleted.</p>
27+
28+
<div class="form-group">
29+
<div class="row">
30+
<div class="col-md-2">
31+
<button type="submit" class="btn btn-danger">Delete account</button>
32+
</div>
33+
</div>
34+
35+
<input type="hidden" name="token" value="{{token}}" />
36+
</div>
37+
{{else}}
38+
<div class="form-group">
39+
<div class="row">
40+
<div class="col-md-12">
41+
<div>
42+
<strong>Token not valid</strong>
43+
</div>
44+
</div>
45+
</div>
46+
</div>
47+
{{/if}}
48+
</form>
49+
</div>
50+
</body>
51+
</html>

default-views/account/delete.hbs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Delete Account</title>
7+
<link rel="stylesheet" href="/common/css/bootstrap.min.css">
8+
<script></script>
9+
</head>
10+
<body>
11+
<div class="container">
12+
<h4>Delete Account</h4>
13+
</div>
14+
<div class="container">
15+
<form method="post" action="/account/delete">
16+
<div class="form-group">
17+
{{#if error}}
18+
<div class="row">
19+
<div class="col-md-12">
20+
<p class="text-danger"><strong>{{error}}</strong></p>
21+
</div>
22+
</div>
23+
{{/if}}
24+
<div class="row">
25+
<div class="col-md-12">
26+
{{#if multiuser}}
27+
<p>Please enter your account name. A delete account link will be
28+
emailed to the address you provided during account registration.</p>
29+
30+
<label for="username">Account Name:</label>
31+
<input type="text" class="form-control" name="username" id="username"
32+
placeholder="alice" />
33+
{{else}}
34+
<p>A delete account link will be
35+
emailed to the address you provided during account registration.</p>
36+
{{/if}}
37+
</div>
38+
</div>
39+
</div>
40+
41+
<div class="form-group">
42+
<div class="row">
43+
<div class="col-md-2">
44+
<button type="submit" class="btn btn-primary">Send Delete Account Link</button>
45+
</div>
46+
</div>
47+
</div>
48+
</form>
49+
</div>
50+
</body>
51+
</html>

lib/api/authn/webid-oidc.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const { LoginRequest } = require('../../requests/login-request')
1111

1212
const PasswordResetEmailRequest = require('../../requests/password-reset-email-request')
1313
const PasswordChangeRequest = require('../../requests/password-change-request')
14+
const DeleteAccountRequest = require('../../requests/delete-account-request')
15+
const DeleteAccountConfirmRequest = require('../../requests/delete-account-confirm-request')
1416

1517
const {
1618
AuthCallbackRequest,
@@ -80,6 +82,12 @@ function middleware (oidc) {
8082
router.get('/account/password/change', PasswordChangeRequest.get)
8183
router.post('/account/password/change', bodyParser, PasswordChangeRequest.post)
8284

85+
router.get('/account/delete', DeleteAccountRequest.get)
86+
router.post('/account/delete', bodyParser, DeleteAccountRequest.post)
87+
88+
router.get('/account/delete/confirm', DeleteAccountConfirmRequest.get)
89+
router.post('/account/delete/confirm', bodyParser, DeleteAccountConfirmRequest.post)
90+
8391
router.get('/logout', LogoutRequest.handle)
8492
router.post('/logout', LogoutRequest.handle)
8593

lib/models/account-manager.js

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,18 @@ class AccountManager {
461461
return resetUrl
462462
}
463463

464+
/**
465+
* Returns a password reset URL (to be emailed to the user upon request)
466+
*
467+
* @param token {string} One-time-use expiring token, via the TokenService
468+
* @param returnToUrl {string}
469+
*
470+
* @return {string}
471+
*/
472+
getAccountDeleteUrl (token) {
473+
return url.resolve(this.host.serverUri, `/account/delete/confirm?token=${token}`)
474+
}
475+
464476
/**
465477
* Parses and returns an account recovery email stored in a user's root .acl
466478
*
@@ -490,19 +502,37 @@ class AccountManager {
490502
})
491503
}
492504

493-
sendPasswordResetEmail (userAccount, returnToUrl) {
505+
verifyEmailDependencies (userAccount) {
506+
if (!this.emailService) {
507+
throw new Error('Email service is not set up')
508+
}
509+
510+
if (!userAccount.email) {
511+
throw new Error('Account recovery email has not been provided')
512+
}
513+
}
514+
515+
sendDeleteAccountEmail (userAccount) {
494516
return Promise.resolve()
495-
.then(() => {
496-
if (!this.emailService) {
497-
throw new Error('Email service is not set up')
498-
}
517+
.then(() => this.verifyEmailDependencies(userAccount))
518+
.then(() => this.generateResetToken(userAccount))
519+
.then(resetToken => {
520+
const deleteUrl = this.getAccountDeleteUrl(resetToken)
499521

500-
if (!userAccount.email) {
501-
throw new Error('Account recovery email has not been provided')
522+
const emailData = {
523+
to: userAccount.email,
524+
webId: userAccount.webId,
525+
deleteUrl: deleteUrl
502526
}
503527

504-
return this.generateResetToken(userAccount)
528+
return this.emailService.sendWithTemplate('delete-account', emailData)
505529
})
530+
}
531+
532+
sendPasswordResetEmail (userAccount, returnToUrl) {
533+
return Promise.resolve()
534+
.then(() => this.verifyEmailDependencies(userAccount))
535+
.then(() => this.generateResetToken(userAccount))
506536
.then(resetToken => {
507537
let resetUrl = this.passwordResetUrl(resetToken, returnToUrl)
508538

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
'use strict'
2+
3+
const AuthRequest = require('./auth-request')
4+
const debug = require('./../debug').accounts
5+
6+
class DeleteAccountConfirmRequest extends AuthRequest {
7+
/**
8+
* @constructor
9+
* @param options {Object}
10+
* @param options.accountManager {AccountManager}
11+
* @param options.userStore {UserStore}
12+
* @param options.response {ServerResponse} express response object
13+
* @param [options.token] {string} One-time reset password token (from email)
14+
*/
15+
constructor (options) {
16+
super(options)
17+
18+
this.token = options.token
19+
this.validToken = false
20+
}
21+
22+
/**
23+
* Factory method, returns an initialized instance of DeleteAccountConfirmRequest
24+
* from an incoming http request.
25+
*
26+
* @param req {IncomingRequest}
27+
* @param res {ServerResponse}
28+
*
29+
* @return {DeleteAccountConfirmRequest}
30+
*/
31+
static fromParams (req, res) {
32+
let locals = req.app.locals
33+
let accountManager = locals.accountManager
34+
let userStore = locals.oidc.users
35+
36+
let token = this.parseParameter(req, 'token')
37+
38+
let options = {
39+
accountManager,
40+
userStore,
41+
token,
42+
response: res
43+
}
44+
45+
return new DeleteAccountConfirmRequest(options)
46+
}
47+
48+
/**
49+
* Handles a Change Password GET request on behalf of a middleware handler.
50+
*
51+
* @param req {IncomingRequest}
52+
* @param res {ServerResponse}
53+
*
54+
* @return {Promise}
55+
*/
56+
static get (req, res) {
57+
const request = DeleteAccountConfirmRequest.fromParams(req, res)
58+
59+
return Promise.resolve()
60+
.then(() => request.validateToken())
61+
.then(() => request.renderForm())
62+
.catch(error => request.error(error))
63+
}
64+
65+
/**
66+
* Handles a Change Password POST request on behalf of a middleware handler.
67+
*
68+
* @param req {IncomingRequest}
69+
* @param res {ServerResponse}
70+
*
71+
* @return {Promise}
72+
*/
73+
static post (req, res) {
74+
const request = DeleteAccountConfirmRequest.fromParams(req, res)
75+
76+
return DeleteAccountConfirmRequest.handlePost(request)
77+
}
78+
79+
/**
80+
* Performs the 'Change Password' operation, after the user submits the
81+
* password change form. Validates the parameters (the one-time token,
82+
* the new password), changes the password, and renders the success view.
83+
*
84+
* @param request {DeleteAccountConfirmRequest}
85+
*
86+
* @return {Promise}
87+
*/
88+
static handlePost (request) {
89+
return Promise.resolve()
90+
.then(() => request.validateToken())
91+
.then(tokenContents => request.deleteAccount(tokenContents))
92+
.then(() => request.renderSuccess())
93+
.catch(error => request.error(error))
94+
}
95+
96+
/**
97+
* Validates the one-time Password Reset token that was emailed to the user.
98+
* If the token service has a valid token saved for the given key, it returns
99+
* the token object value (which contains the user's WebID URI, etc).
100+
* If no token is saved, returns `false`.
101+
*
102+
* @return {Promise<Object|false>}
103+
*/
104+
validateToken () {
105+
return Promise.resolve()
106+
.then(() => {
107+
if (!this.token) { return false }
108+
109+
return this.accountManager.validateResetToken(this.token)
110+
})
111+
.then(validToken => {
112+
if (validToken) {
113+
this.validToken = true
114+
}
115+
116+
return validToken
117+
})
118+
.catch(error => {
119+
this.token = null
120+
throw error
121+
})
122+
}
123+
124+
/**
125+
* Changes the password that's saved in the user store.
126+
* If the user has no user store entry, it creates one.
127+
*
128+
* @param tokenContents {Object}
129+
* @param tokenContents.webId {string}
130+
*
131+
* @return {Promise}
132+
*/
133+
deleteAccount (tokenContents) {
134+
let user = this.accountManager.userAccountFrom(tokenContents)
135+
136+
debug('Delete account for user:', user.webId)
137+
138+
return this.userStore.findUser(user.id)
139+
.then(userStoreEntry => {
140+
// TODO: @kjetilk delete the user here
141+
})
142+
}
143+
144+
/**
145+
* Renders the 'change password' form.
146+
*
147+
* @param [error] {Error} Optional error to display
148+
*/
149+
renderForm (error) {
150+
let params = {
151+
validToken: this.validToken,
152+
token: this.token
153+
}
154+
155+
if (error) {
156+
params.error = error.message
157+
this.response.status(error.statusCode)
158+
}
159+
160+
this.response.render('account/delete-confirm', params)
161+
}
162+
163+
/**
164+
* Displays the 'password has been changed' success view.
165+
*/
166+
renderSuccess () {
167+
this.response.render('account/account-deleted')
168+
}
169+
}
170+
171+
module.exports = DeleteAccountConfirmRequest

0 commit comments

Comments
 (0)