Coverage for portality/view/account.py: 42%

232 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-08-04 15:38 +0100

1import uuid, json 

2 

3from flask import Blueprint, request, url_for, flash, redirect, make_response 

4from flask import render_template, abort 

5from flask_login import login_user, logout_user, current_user, login_required 

6from wtforms import StringField, HiddenField, PasswordField, validators, Form 

7 

8from portality import util 

9from portality import constants 

10from portality.core import app 

11from portality.decorators import ssl_required, write_required 

12from portality.models import Account, Event 

13from portality.forms.validate import DataOptional, EmailAvailable, ReservedUsernames, IdAvailable, IgnoreUnchanged 

14from portality.bll import DOAJ 

15 

16blueprint = Blueprint('account', __name__) 

17 

18 

19@blueprint.route('/') 

20@login_required 

21@ssl_required 

22def index(): 

23 if not current_user.has_role("list_users"): 

24 abort(401) 

25 return render_template("account/users.html") 

26 

27 

28class UserEditForm(Form): 

29 

30 # Let's not allow anyone to change IDs - there lies madness and destruction (referential integrity) 

31 # id = StringField('ID', [IgnoreUnchanged(), ReservedUsernames(), IdAvailable()]) 

32 

33 name = StringField('Account name', [DataOptional(), validators.Length(min=3, max=64)]) 

34 email = StringField('Email address', [ 

35 IgnoreUnchanged(), 

36 validators.Length(min=3, max=254), 

37 validators.Email(message='Must be a valid email address'), 

38 EmailAvailable(), 

39 validators.EqualTo('email_confirm', message='Email confirmation must match'), 

40 ]) 

41 email_confirm = StringField('Confirm email address') 

42 roles = StringField('User roles') 

43 password_change = PasswordField('Change password', [ 

44 validators.EqualTo('password_confirm', message='Passwords must match'), 

45 ]) 

46 password_confirm = PasswordField('Confirm password') 

47 

48 

49@blueprint.route('/<username>', methods=['GET', 'POST', 'DELETE']) 

50@login_required 

51@ssl_required 

52@write_required() 

53def username(username): 

54 acc = Account.pull(username) 

55 

56 if acc is None: 

57 abort(404) 

58 if (request.method == 'DELETE' or 

59 (request.method == 'POST' and request.values.get('submit', False) == 'Delete')): 

60 if current_user.id != acc.id and not current_user.is_super: 

61 abort(401) 

62 else: 

63 conf = request.values.get("delete_confirm") 

64 if conf is None or conf != "delete_confirm": 

65 flash('Check the box to confirm you really mean it!', "error") 

66 return render_template('account/view.html', account=acc, form=UserEditForm(obj=acc)) 

67 acc.delete() 

68 flash('Account ' + acc.id + ' deleted') 

69 return redirect(url_for('.index')) 

70 

71 elif request.method == 'POST': 

72 if current_user.id != acc.id and not current_user.is_super: 

73 abort(401) 

74 

75 form = UserEditForm(obj=acc, formdata=request.form) 

76 

77 if not form.validate(): 

78 return render_template('account/view.html', account=acc, form=form) 

79 

80 newdata = request.json if request.json else request.values 

81 if request.values.get('submit', False) == 'Generate a new API Key': 

82 acc.generate_api_key() 

83 

84 # if 'id' in newdata and len(newdata['id']) > 0: 

85 # if newdata['id'] != current_user.id == acc.id: 

86 # flash('You may not edit the ID of your own account', 'error') 

87 # return render_template('account/view.html', account=acc, form=form) 

88 # else: 

89 # acc.delete() # request for the old record to be deleted from ES 

90 # acc.set_id(newdata['id']) 

91 

92 if 'name' in newdata: 

93 acc.set_name(newdata['name']) 

94 if 'password_change' in newdata and len(newdata['password_change']) > 0 and not newdata['password_change'].startswith('sha1'): 

95 acc.set_password(newdata['password_change']) 

96 

97 # only super users can re-write roles 

98 if "roles" in newdata and current_user.is_super: 

99 new_roles = [r.strip() for r in newdata.get("roles").split(",")] 

100 acc.set_role(new_roles) 

101 

102 if "marketing_consent" in newdata: 

103 acc.set_marketing_consent(newdata["marketing_consent"] == "true") 

104 

105 if 'email' in newdata and len(newdata['email']) > 0 and newdata['email'] != acc.email: 

106 acc.set_email(newdata['email']) 

107 

108 # If the user updated their own email address, invalidate the password and require verification again. 

109 if current_user.id == acc.id: 

110 acc.clear_password() 

111 reset_token = uuid.uuid4().hex 

112 acc.set_reset_token(reset_token, app.config.get("PASSWORD_RESET_TIMEOUT", 86400)) 

113 acc.save() 

114 

115 events_svc = DOAJ.eventsService() 

116 events_svc.trigger(Event(constants.EVENT_ACCOUNT_PASSWORD_RESET, acc.id, context={"account" : acc.data})) 

117 flash("Email address updated. You have been logged out for email address verification.") 

118 

119 logout_user() 

120 

121 if app.config.get('DEBUG', False): 

122 reset_url = url_for('account.reset', reset_token=acc.reset_token) 

123 util.flash_with_url('Debug mode - url for reset is <a href={0}>{0}</a>'.format(reset_url)) 

124 

125 return redirect(url_for('doaj.home')) 

126 

127 acc.save() 

128 flash("Record updated") 

129 return render_template('account/view.html', account=acc, form=form) 

130 

131 else: # GET 

132 if util.request_wants_json(): 

133 resp = make_response( 

134 json.dumps(acc.data, sort_keys=True, indent=4)) 

135 resp.mimetype = "application/json" 

136 return resp 

137 else: 

138 form = UserEditForm(obj=acc) 

139 return render_template('account/view.html', account=acc, form=form) 

140 

141 

142def get_redirect_target(form=None, acc=None): 

143 form_target = '' 

144 if form and hasattr(form, 'next') and getattr(form, 'next'): 

145 form_target = form.next.data 

146 

147 for target in form_target, request.args.get('next', []): 

148 if not target: 

149 continue 

150 if target == util.is_safe_url(target): 

151 return target 

152 

153 if acc is None: 

154 return "" 

155 

156 destinations = app.config.get("ROLE_LOGIN_DESTINATIONS") 

157 for role, dest in destinations: 

158 if acc.has_role(role): 

159 return url_for(dest) 

160 

161 return url_for(app.config.get("DEFAULT_LOGIN_DESTINATION")) 

162 

163 

164class RedirectForm(Form): 

165 next = HiddenField() 

166 

167 def __init__(self, *args, **kwargs): 

168 Form.__init__(self, *args, **kwargs) 

169 if not self.next.data: 

170 self.next.data = get_redirect_target() or '' 

171 

172 def redirect(self, endpoint='index', **values): 

173 if self.next.data == util.is_safe_url(self.next.data): 

174 return redirect(self.next.data) 

175 target = get_redirect_target() 

176 return redirect(target or url_for(endpoint, **values)) 

177 

178 

179class LoginForm(RedirectForm): 

180 user = StringField('Email address or username', [validators.DataRequired()]) 

181 password = PasswordField('Password', [validators.DataRequired()]) 

182 

183 

184@blueprint.route('/login', methods=['GET', 'POST']) 

185@ssl_required 

186def login(): 

187 current_info = {'next': request.args.get('next', '')} 

188 form = LoginForm(request.form, csrf_enabled=False, **current_info) 

189 if request.method == 'POST' and form.validate(): 

190 password = form.password.data 

191 username = form.user.data 

192 

193 # If our settings allow, try getting the user account by ID first, then by email address 

194 if app.config.get('LOGIN_VIA_ACCOUNT_ID', False): 

195 user = Account.pull(username) or Account.pull_by_email(username) 

196 else: 

197 user = Account.pull_by_email(username) 

198 

199 # If we have a verified user account, proceed to attempt login 

200 try: 

201 if user is not None: 

202 if user.check_password(password): 

203 login_user(user, remember=True) 

204 flash('Welcome back.', 'success') 

205 return redirect(get_redirect_target(form=form, acc=user)) 

206 else: 

207 form.password.errors.append('The password you entered is incorrect. Try again or <a href="{0}">reset your password</a>.'.format(url_for(".forgot"))) 

208 else: 

209 form.user.errors.append('Account not recognised. If you entered an email address, try your username instead.') 

210 except KeyError: 

211 # Account has no password set, the user needs to reset or use an existing valid reset link 

212 FORGOT_INSTR = '<a href="{url}">&lt;click here&gt;</a> to send a new reset link.'.format(url=url_for('.forgot')) 

213 util.flash_with_url('Account verification is incomplete. Check your emails for the link or ' + FORGOT_INSTR, 

214 'error') 

215 return redirect(url_for('doaj.home')) 

216 

217 if request.args.get("redirected") == "apply": 

218 form['next'].data = url_for("apply.public_application") 

219 return render_template('account/login_to_apply.html', form=form) 

220 return render_template('account/login.html', form=form) 

221 

222 

223@blueprint.route('/forgot', methods=['GET', 'POST']) 

224@ssl_required 

225@write_required() 

226def forgot(): 

227 CONTACT_INSTR = ' Please <a href="{url}">contact us.</a>'.format(url=url_for('doaj.contact')) 

228 if request.method == 'POST': 

229 # get hold of the user account 

230 un = request.form.get('un', "") 

231 if app.config.get('LOGIN_VIA_ACCOUNT_ID', False): 

232 account = Account.pull(un) or Account.pull_by_email(un) 

233 else: 

234 account = Account.pull_by_email(un) 

235 

236 if account is None: 

237 util.flash_with_url('Error - your account username / email address is not recognised.' + CONTACT_INSTR, 

238 'error') 

239 return render_template('account/forgot.html') 

240 

241 if not account.data.get('email'): 

242 util.flash_with_url('Error - your account does not have an associated email address.' + CONTACT_INSTR, 

243 'error') 

244 return render_template('account/forgot.html') 

245 

246 # if we get to here, we have a user account to reset 

247 reset_token = uuid.uuid4().hex 

248 account.set_reset_token(reset_token, app.config.get("PASSWORD_RESET_TIMEOUT", 86400)) 

249 account.save() 

250 

251 events_svc = DOAJ.eventsService() 

252 events_svc.trigger(Event(constants.EVENT_ACCOUNT_PASSWORD_RESET, account.id, context={"account": account.data})) 

253 flash('Instructions to reset your password have been sent to you. Please check your emails.') 

254 

255 if app.config.get('DEBUG', False): 

256 util.flash_with_url('Debug mode - url for reset is <a href={0}>{0}</a>'.format( 

257 url_for('account.reset', reset_token=account.reset_token))) 

258 

259 return render_template('account/forgot.html') 

260 

261 

262class ResetForm(Form): 

263 password = PasswordField('Password', [ 

264 validators.DataRequired(), 

265 validators.EqualTo('confirm', message='Passwords must match') 

266 ]) 

267 confirm = PasswordField('Repeat Password') 

268 

269 

270@blueprint.route("/reset/<reset_token>", methods=["GET", "POST"]) 

271@ssl_required 

272@write_required() 

273def reset(reset_token): 

274 form = ResetForm(request.form, csrf_enabled=False) 

275 account = Account.get_by_reset_token(reset_token) 

276 if account is None: 

277 abort(404) 

278 

279 if request.method == "POST" and form.validate(): 

280 # check that the passwords match, and bounce if not 

281 pw = request.values.get("password") 

282 conf = request.values.get("confirm") 

283 if pw != conf: 

284 flash("Passwords do not match - please try again", "error") 

285 return render_template("account/reset.html", account=account, form=form) 

286 

287 # update the user's account 

288 account.set_password(pw) 

289 account.remove_reset_token() 

290 account.save() 

291 flash("New password has been set and you're now logged in.", "success") 

292 

293 # log the user in 

294 login_user(account, remember=True) 

295 return redirect(url_for('doaj.home')) 

296 

297 return render_template("account/reset.html", account=account, form=form) 

298 

299 

300@blueprint.route('/logout') 

301@ssl_required 

302def logout(): 

303 logout_user() 

304 flash('You are now logged out', 'success') 

305 return redirect('/') 

306 

307 

308class RegisterForm(RedirectForm): 

309 identifier = StringField('ID', [ReservedUsernames(), IdAvailable()]) 

310 name = StringField('Name', [validators.Optional(), validators.Length(min=3, max=64)]) 

311 email = StringField('Email address', [ 

312 validators.DataRequired(), 

313 validators.Length(min=3, max=254), 

314 validators.Email(message='Must be a valid email address'), 

315 EmailAvailable(message="That email address is already in use. Please <a href='/account/forgot'>reset your password</a>. If you still cannot login, <a href='/contact'>contact us</a>.") 

316 ]) 

317 roles = StringField('Roles') 

318 recaptcha_value = HiddenField() 

319 

320 

321@blueprint.route('/register', methods=['GET', 'POST']) 

322@ssl_required 

323@write_required() 

324def register(): 

325 # 3rd-party registration only for those with create_user role, only allow public registration when configured 

326 if current_user.is_authenticated and not current_user.has_role("create_user") \ 

327 or current_user.is_anonymous and app.config.get('PUBLIC_REGISTER', False) is False: 

328 abort(401) # todo: we may need a template to explain this since it's linked from the application form 

329 

330 form = RegisterForm(request.form, csrf_enabled=False, roles='api,publisher', identifier=Account.new_short_uuid()) 

331 if request.method == 'POST' and form.validate(): 

332 if app.config.get("RECAPTCHA_ENABLE"): 

333 recap_data = util.verify_recaptcha(form.recaptcha_value.data) 

334 else: 

335 recap_data = {"success": True} 

336 if recap_data["success"]: 

337 account = Account.make_account(email=form.email.data, username=form.identifier.data, name=form.name.data, 

338 roles=[r.strip() for r in form.roles.data.split(',')]) 

339 account.save() 

340 

341 event_svc = DOAJ.eventsService() 

342 event_svc.trigger(Event(constants.EVENT_ACCOUNT_CREATED, account.id, context={"account" : account.data})) 

343 # send_account_created_email(account) 

344 

345 if app.config.get('DEBUG', False): 

346 util.flash_with_url('Debug mode - url for verify is <a href={0}>{0}</a>'.format(url_for('account.reset', reset_token=account.reset_token))) 

347 

348 if current_user.is_authenticated: 

349 util.flash_with_url('Account created for {0}. View Account: <a href={1}>{1}</a>'.format(account.email, url_for('.username', username=account.id))) 

350 return redirect(url_for('.index')) 

351 else: 

352 flash('Thank you, please verify email address ' + form.email.data + ' to set your password and login.', 

353 'success') 

354 

355 # We must redirect home because the user now needs to verify their email address. 

356 return redirect(url_for('doaj.home')) 

357 

358 else: # recaptcha fail 

359 util.flash("reCAPTCHA failed, please retry.") 

360 

361 if request.method == 'POST' and not form.validate(): 

362 flash('Please correct the errors', 'error') 

363 return render_template('account/register.html', form=form)