diff --git a/amanuensis/cli/lexicon.py b/amanuensis/cli/lexicon.py index 66c662b..eacf099 100644 --- a/amanuensis/cli/lexicon.py +++ b/amanuensis/cli/lexicon.py @@ -17,11 +17,10 @@ def command_create(args): settings are as desired before opening the lexicon for player joins. """ from lexicon.manage import create_lexicon - import user + from user import UserModel # TODO verify args - uid = user.uid_from_username(args.editor) - u = user.user_from_uid(uid) - create_lexicon(args.name, u) + editor = UserModel.by(name=args.editor) + create_lexicon(args.name, editor) @requires_lexicon def command_delete(args): diff --git a/amanuensis/cli/user.py b/amanuensis/cli/user.py index f33a5f5..17218ec 100644 --- a/amanuensis/cli/user.py +++ b/amanuensis/cli/user.py @@ -2,9 +2,9 @@ from cli.helpers import ( add_argument, no_argument, requires_username, config_get, config_set, CONFIG_GET_ROOT_VALUE) -@add_argument("--username", help="User's login handle") +@requires_username +@add_argument("--email", required=True, help="User's email") @add_argument("--displayname", help="User's publicly displayed name") -@add_argument("--email", help="User's email") def command_create(args): """ Create a user @@ -15,18 +15,14 @@ def command_create(args): import config # Verify or query parameters - if not args.username: - args.username = input("username: ").strip() if not user.valid_username(args.username): config.logger.error("Invalid username: usernames may only contain alphanumeric characters, dashes, and underscores") return -1 - if user.uid_from_username(args.username) is not None: + if user.UserModel.by(name=args.username) is not None: config.logger.error("Invalid username: username is already taken") return -1 if not args.displayname: args.displayname = args.username - if not args.email: - args.email = input("email: ").strip() if not user.valid_email(args.email): config.logger.error("Invalid email") return -1 @@ -37,7 +33,7 @@ def command_create(args): print(json.dumps(js, indent=2)) print("Username: {}\nUser ID: {}\nPassword: {}".format(args.username, new_user.uid, tmp_pw)) -@add_argument("--id", help="id of user to delete") +@add_argument("--id", required=True, help="id of user to delete") def command_delete(args): """ Delete a user @@ -84,23 +80,23 @@ def command_config(args): """ import json import config - import user + from user import UserModel if args.get and args.set: config.logger.error("Specify one of --get and --set") return -1 - uid = user.uid_from_username(args.username) - if not uid: + u = UserModel.by(name=args.username) + if not u: config.logger.error("User not found") return -1 if args.get: - with config.json_ro('user', uid, 'config.json') as cfg: + with config.json_ro('user', u.id, 'config.json') as cfg: config_get(cfg, args.get) if args.set: - with config.json_rw('user', uid, 'config.json') as cfg: + with config.json_rw('user', u.id, 'config.json') as cfg: config_set(cfg, args.set) @add_argument("--username", help="The user to change password for") @@ -112,14 +108,13 @@ def command_passwd(args): import os import config - import user + from user import UserModel if not args.username: args.username = input("Username: ") - uid = user.uid_from_username(args.username) - if uid is None: + u = UserModel.by(name=args.username) + if u is None: config.logger.error("No user with username '{}'".format(args.username)) return -1 - u = user.user_from_uid(uid) pw = getpass.getpass("Password: ") u.set_password(pw) diff --git a/amanuensis/server/auth.py b/amanuensis/server/auth.py index 6c2d2c8..f719642 100644 --- a/amanuensis/server/auth.py +++ b/amanuensis/server/auth.py @@ -5,7 +5,7 @@ from wtforms.validators import DataRequired from flask_login import current_user, login_user, logout_user, login_required import config -import user +from user import UserModel class LoginForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) @@ -19,25 +19,21 @@ def get_bp(login_manager): @login_manager.user_loader def load_user(uid): - return user.user_from_uid(str(uid)) + return UserModel.by(uid=str(uid)) @bp.route('/login/', methods=['GET', 'POST']) def login(): form = LoginForm() if form.validate_on_submit(): username = form.username.data - uid = user.uid_from_username(username) - if uid is not None: - u = user.user_from_uid(uid) - if u.check_password(form.password.data): - remember_me = form.remember.data - login_user(u, remember=remember_me) - config.logger.info("Logged in user '{}' ({})".format( - u.username, u.uid)) - return redirect(url_for('home.home')) + u = UserModel.by(name=username) + if u is not None and u.check_password(form.password.data): + remember_me = form.remember.data + login_user(u, remember=remember_me) + config.logger.info("Logged in user '{}' ({})".format( + u.username, u.uid)) + return redirect(url_for('home.home')) flash("Login not recognized") - else: - pass return render_template('auth/login.html', form=form) @bp.route("/logout/", methods=['GET']) diff --git a/amanuensis/user.py b/amanuensis/user.py index 208d76a..24dbebd 100644 --- a/amanuensis/user.py +++ b/amanuensis/user.py @@ -6,15 +6,37 @@ import uuid from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash +from errors import InternalMisuseError, MissingConfigError, IndexMismatchError import config import resources -class User(UserMixin): - def __init__(self, uid): +class UserModel(UserMixin): + @staticmethod + def by(uid=None, name=None): + """ + Gets the UserModel with the given uid or username + + If the uid or name simply does not match an existing user, returns + None. If the uid matches the index but there is something wrong with + the user's config, raises an error. + """ + if uid and name: + raise InternalMisuseError("uid and name both specified to UserModel.by()") + if not uid and not name: + raise ValueError("One of uid or name must be not None") + if not uid: + with config.json_ro('user', 'index.json') as index: + uid = index.get(name) + if not uid: + return None if not os.path.isdir(config.prepend('user', uid)): - raise ValueError("No user with uid {}".format(uid)) + raise IndexMismatchError("username={} uid={}".format(name, uid)) if not os.path.isfile(config.prepend('user', uid, 'config.json')): - raise FileNotFoundError("User {} missing config.json".format(uid)) + raise MissingConfigError("uid={}".format(uid)) + return UserModel(uid) + + def __init__(self, uid): + """User model initializer, assume all checks were done by by()""" self.id = str(uid) # Flask-Login checks for this self.config_path = config.prepend('user', uid, 'config.json') with config.json_ro(self.config_path) as j: @@ -76,25 +98,7 @@ def create_user(username, displayname, email): # Set a temporary password temp_pw = os.urandom(32).hex() - u = User(uid) + u = UserModel.by(uid=uid) u.set_password(temp_pw) return u, temp_pw - -def uid_from_username(username): - """Gets the internal uid of a user given a username""" - if username is None: - raise ValueError("username must not be None") - if not username: - raise ValueError("username must not be empty") - with config.json_ro('user', 'index.json') as index: - uid = index.get(username) - if uid is None: - config.logger.debug("uid_from_username('{}') returned None".format(username)) - return uid - -def user_from_uid(uid): - if not os.path.isdir(config.prepend('user', uid)): - config.logger.debug("No user with uid '{}'".format(uid)) - return None - return User(uid)