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
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-04 15:38 +0100
1import uuid, json
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
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
16blueprint = Blueprint('account', __name__)
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")
28class UserEditForm(Form):
30 # Let's not allow anyone to change IDs - there lies madness and destruction (referential integrity)
31 # id = StringField('ID', [IgnoreUnchanged(), ReservedUsernames(), IdAvailable()])
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')
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)
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'))
71 elif request.method == 'POST':
72 if current_user.id != acc.id and not current_user.is_super:
73 abort(401)
75 form = UserEditForm(obj=acc, formdata=request.form)
77 if not form.validate():
78 return render_template('account/view.html', account=acc, form=form)
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()
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'])
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'])
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)
102 if "marketing_consent" in newdata:
103 acc.set_marketing_consent(newdata["marketing_consent"] == "true")
105 if 'email' in newdata and len(newdata['email']) > 0 and newdata['email'] != acc.email:
106 acc.set_email(newdata['email'])
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()
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.")
119 logout_user()
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))
125 return redirect(url_for('doaj.home'))
127 acc.save()
128 flash("Record updated")
129 return render_template('account/view.html', account=acc, form=form)
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)
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
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
153 if acc is None:
154 return ""
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)
161 return url_for(app.config.get("DEFAULT_LOGIN_DESTINATION"))
164class RedirectForm(Form):
165 next = HiddenField()
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 ''
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))
179class LoginForm(RedirectForm):
180 user = StringField('Email address or username', [validators.DataRequired()])
181 password = PasswordField('Password', [validators.DataRequired()])
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
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)
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}"><click here></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'))
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)
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)
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')
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')
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()
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.')
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)))
259 return render_template('account/forgot.html')
262class ResetForm(Form):
263 password = PasswordField('Password', [
264 validators.DataRequired(),
265 validators.EqualTo('confirm', message='Passwords must match')
266 ])
267 confirm = PasswordField('Repeat Password')
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)
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)
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")
293 # log the user in
294 login_user(account, remember=True)
295 return redirect(url_for('doaj.home'))
297 return render_template("account/reset.html", account=account, form=form)
300@blueprint.route('/logout')
301@ssl_required
302def logout():
303 logout_user()
304 flash('You are now logged out', 'success')
305 return redirect('/')
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()
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
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()
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)
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)))
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')
355 # We must redirect home because the user now needs to verify their email address.
356 return redirect(url_for('doaj.home'))
358 else: # recaptcha fail
359 util.flash("reCAPTCHA failed, please retry.")
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)