# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 # (only), as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # # This program is dual-licensed. If you wish to learn more about the # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ import re import colander from rhodecode import forms from rhodecode.model.db import User, UserEmailMap from rhodecode.model.validation_schema import types, validators from rhodecode.translation import _ from rhodecode.lib.auth import check_password from rhodecode.lib import helpers as h @colander.deferred def deferred_user_password_validator(node, kw): username = kw.get('username') user = User.get_by_username(username) def _user_password_validator(node, value): if not check_password(value, user.password): msg = _('Password is incorrect') raise colander.Invalid(node, msg) return _user_password_validator class ChangePasswordSchema(colander.Schema): current_password = colander.SchemaNode( colander.String(), missing=colander.required, widget=forms.widget.PasswordWidget(redisplay=True), validator=deferred_user_password_validator) new_password = colander.SchemaNode( colander.String(), missing=colander.required, widget=forms.widget.CheckedPasswordWidget(redisplay=True), validator=colander.Length(min=6)) def validator(self, form, values): if values['current_password'] == values['new_password']: exc = colander.Invalid(form) exc['new_password'] = _('New password must be different ' 'to old password') raise exc @colander.deferred def deferred_username_validator(node, kw): old_username = kw.get('username') def name_validator(node, value): msg = _( 'Username may only contain alphanumeric characters ' 'underscores, periods or dashes and must begin with ' 'alphanumeric character or underscore') if not re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value): raise colander.Invalid(node, msg) if value != old_username: existing_user = User.get_by_username(value, case_insensitive=True) if existing_user: raise colander.Invalid(node, 'Username is already taken') return name_validator @colander.deferred def deferred_email_validator(node, kw): # NOTE(marcink): we might provide uniqueness validation later here... return colander.Email() class UserSchema(colander.Schema): username = colander.SchemaNode( colander.String(), validator=deferred_username_validator) email = colander.SchemaNode( colander.String(), validator=deferred_email_validator) password = colander.SchemaNode( colander.String(), missing='') first_name = colander.SchemaNode( colander.String(), missing='') last_name = colander.SchemaNode( colander.String(), missing='') description = colander.SchemaNode( colander.String(), missing='') active = colander.SchemaNode( types.StringBooleanType(), missing=False) admin = colander.SchemaNode( types.StringBooleanType(), missing=False) extern_name = colander.SchemaNode( colander.String(), missing='') extern_type = colander.SchemaNode( colander.String(), missing='') def deserialize(self, cstruct): """ Custom deserialize that allows to chain validation, and verify permissions, and as last step uniqueness """ appstruct = super().deserialize(cstruct) return appstruct @colander.deferred def deferred_user_email_in_emails_validator(node, kw): return colander.OneOf(kw.get('user_emails')) @colander.deferred def deferred_additional_email_validator(node, kw): emails = kw.get('user_emails') def name_validator(node, value): if value in emails: msg = _('This e-mail address is already taken') raise colander.Invalid(node, msg) user = User.get_by_email(value, case_insensitive=True) if user: msg = _('This e-mail address is already taken') raise colander.Invalid(node, msg) c = colander.Email() return c(node, value) return name_validator @colander.deferred def deferred_user_email_in_emails_widget(node, kw): import deform.widget emails = [(email, email) for email in kw.get('user_emails')] return deform.widget.Select2Widget(values=emails) class UserProfileSchema(colander.Schema): username = colander.SchemaNode( colander.String(), validator=deferred_username_validator) firstname = colander.SchemaNode( colander.String(), missing='', title='First name') lastname = colander.SchemaNode( colander.String(), missing='', title='Last name') description = colander.SchemaNode( colander.String(), missing='', title='Personal Description', widget=forms.widget.TextAreaWidget(), validator=colander.Length(max=250) ) email = colander.SchemaNode( colander.String(), widget=deferred_user_email_in_emails_widget, validator=deferred_user_email_in_emails_validator, description=h.literal( _('Additional emails can be specified at extra emails page.').format( '/_admin/my_account/emails')), ) class AddEmailSchema(colander.Schema): current_password = colander.SchemaNode( colander.String(), missing=colander.required, widget=forms.widget.PasswordWidget(redisplay=True), validator=deferred_user_password_validator) email = colander.SchemaNode( colander.String(), title='New Email', validator=deferred_additional_email_validator)