Prototype: OTP auth, styleguide overhaul, login/session rework
Replace password reset with email OTP verification flow. Add auth_otp module, OTP templates, and email delivery. Expand styleguide CSS with full component library. Rework login, register, and admin views for cookie sessions. Remove legacy 2FA templates and password reset flow. Update SSH wrappers, forms, validators, and middleware.
This commit is contained in:
parent
1dac65fc8a
commit
722c3bd369
38 changed files with 4100 additions and 1835 deletions
|
|
@ -149,7 +149,7 @@ app.base_url = http://rhodecode.local
|
||||||
app.service_api.host = http://rhodecode.local:10020
|
app.service_api.host = http://rhodecode.local:10020
|
||||||
|
|
||||||
; Secret for Service API authentication.
|
; Secret for Service API authentication.
|
||||||
app.service_api.token =
|
app.service_api.token = oFK71R0pWjdtSukV0ajcnfVxe4j2ExWc7otEKvRPaks
|
||||||
|
|
||||||
; Unique application ID. Should be a random unique string for security.
|
; Unique application ID. Should be a random unique string for security.
|
||||||
app_instance_uuid = rc-production
|
app_instance_uuid = rc-production
|
||||||
|
|
@ -254,7 +254,7 @@ auth_ret_code_detection = false
|
||||||
lock_ret_code = 423
|
lock_ret_code = 423
|
||||||
|
|
||||||
; Filesystem location were repositories should be stored
|
; Filesystem location were repositories should be stored
|
||||||
repo_store.path = /var/opt/rhodecode_repo_store
|
repo_store.path = /home/fox/git/rhodecode-enterprise-ce/.dev/repo_store
|
||||||
|
|
||||||
; allows to setup custom hooks in settings page
|
; allows to setup custom hooks in settings page
|
||||||
allow_custom_hooks_settings = true
|
allow_custom_hooks_settings = true
|
||||||
|
|
@ -309,7 +309,7 @@ labs_settings_active = true
|
||||||
; NOTE: this setting IS DEPRECATED:
|
; NOTE: this setting IS DEPRECATED:
|
||||||
; file_store.storage_path = X -> use `file_store.filesystem_v2.storage_path = X` instead
|
; file_store.storage_path = X -> use `file_store.filesystem_v2.storage_path = X` instead
|
||||||
; path to store the uploaded binaries and artifacts
|
; path to store the uploaded binaries and artifacts
|
||||||
#file_store.storage_path = /var/opt/rhodecode_data/file_store
|
#file_store.storage_path = /home/fox/git/rhodecode-enterprise-ce/.dev/data/file_store
|
||||||
|
|
||||||
; Artifacts file-store, is used to store comment attachments and artifacts uploads.
|
; Artifacts file-store, is used to store comment attachments and artifacts uploads.
|
||||||
; file_store backend type: filesystem_v1, filesystem_v2 or objectstore (s3-based) are available as options
|
; file_store backend type: filesystem_v1, filesystem_v2 or objectstore (s3-based) are available as options
|
||||||
|
|
@ -319,10 +319,10 @@ labs_settings_active = true
|
||||||
#file_store.backend.type = filesystem_v2
|
#file_store.backend.type = filesystem_v2
|
||||||
|
|
||||||
; filesystem options...
|
; filesystem options...
|
||||||
#file_store.filesystem_v1.storage_path = /var/opt/rhodecode_data/artifacts_file_store
|
#file_store.filesystem_v1.storage_path = /home/fox/git/rhodecode-enterprise-ce/.dev/data/artifacts_file_store
|
||||||
|
|
||||||
; filesystem_v2 options...
|
; filesystem_v2 options...
|
||||||
#file_store.filesystem_v2.storage_path = /var/opt/rhodecode_data/artifacts_file_store
|
#file_store.filesystem_v2.storage_path = /home/fox/git/rhodecode-enterprise-ce/.dev/data/artifacts_file_store
|
||||||
#file_store.filesystem_v2.shards = 8
|
#file_store.filesystem_v2.shards = 8
|
||||||
|
|
||||||
; objectstore options...
|
; objectstore options...
|
||||||
|
|
@ -389,7 +389,7 @@ archive_cache.objectstore.retry_attempts = 10
|
||||||
; and served from the cache during subsequent requests for the same archive of
|
; and served from the cache during subsequent requests for the same archive of
|
||||||
; the repository. This path is important to be shared across filesystems and with
|
; the repository. This path is important to be shared across filesystems and with
|
||||||
; RhodeCode and vcsserver
|
; RhodeCode and vcsserver
|
||||||
archive_cache.filesystem.store_dir = /var/opt/rhodecode_data/archive_cache
|
archive_cache.filesystem.store_dir = /home/fox/git/rhodecode-enterprise-ce/.dev/data/archive_cache
|
||||||
|
|
||||||
; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
|
; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
|
||||||
archive_cache.filesystem.cache_size_gb = 1
|
archive_cache.filesystem.cache_size_gb = 1
|
||||||
|
|
@ -455,7 +455,7 @@ celery.task_always_eager = false
|
||||||
|
|
||||||
; Default cache dir for caches. Putting this into a ramdisk can boost performance.
|
; Default cache dir for caches. Putting this into a ramdisk can boost performance.
|
||||||
; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
|
; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
|
||||||
cache_dir = /var/opt/rhodecode_data
|
cache_dir = /home/fox/git/rhodecode-enterprise-ce/.dev/data
|
||||||
|
|
||||||
; *********************************************
|
; *********************************************
|
||||||
; `sql_cache_short` cache for heavy SQL queries
|
; `sql_cache_short` cache for heavy SQL queries
|
||||||
|
|
@ -644,7 +644,7 @@ channelstream.server = channelstream:8000
|
||||||
; For development, comment this out to use auto-generated proxy URL
|
; For development, comment this out to use auto-generated proxy URL
|
||||||
;channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
|
;channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
|
||||||
channelstream.secret = ENV_GENERATED
|
channelstream.secret = ENV_GENERATED
|
||||||
channelstream.history.location = /var/opt/rhodecode_data/channelstream_history
|
channelstream.history.location = /home/fox/git/rhodecode-enterprise-ce/.dev/data/channelstream_history
|
||||||
|
|
||||||
; Internal application path that Javascript uses to connect into.
|
; Internal application path that Javascript uses to connect into.
|
||||||
; If you use proxy-prefix the prefix should be added before /_channelstream
|
; If you use proxy-prefix the prefix should be added before /_channelstream
|
||||||
|
|
@ -729,10 +729,10 @@ vcs.connection_timeout = 3600
|
||||||
vcs.methods.cache = true
|
vcs.methods.cache = true
|
||||||
|
|
||||||
; Filesystem location where Git lfs objects should be stored
|
; Filesystem location where Git lfs objects should be stored
|
||||||
vcs.git.lfs.storage_location = /var/opt/rhodecode_repo_store/.cache/git_lfs_store
|
vcs.git.lfs.storage_location = /home/fox/git/rhodecode-enterprise-ce/.dev/repo_store/.cache/git_lfs_store
|
||||||
|
|
||||||
; Filesystem location where Mercurial largefile objects should be stored
|
; Filesystem location where Mercurial largefile objects should be stored
|
||||||
vcs.hg.largefiles.storage_location = /var/opt/rhodecode_repo_store/.cache/hg_largefiles_store
|
vcs.hg.largefiles.storage_location = /home/fox/git/rhodecode-enterprise-ce/.dev/repo_store/.cache/hg_largefiles_store
|
||||||
|
|
||||||
; ####################################################
|
; ####################################################
|
||||||
; Subversion proxy support (mod_dav_svn)
|
; Subversion proxy support (mod_dav_svn)
|
||||||
|
|
|
||||||
9
package-lock.json
generated
9
package-lock.json
generated
|
|
@ -79,7 +79,6 @@
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -1994,7 +1993,6 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -2021,7 +2019,6 @@
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
|
|
@ -2249,7 +2246,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -3438,7 +3434,6 @@
|
||||||
"integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==",
|
"integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dateformat": "~4.6.2",
|
"dateformat": "~4.6.2",
|
||||||
"eventemitter2": "~0.4.13",
|
"eventemitter2": "~0.4.13",
|
||||||
|
|
@ -5103,7 +5098,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -5829,7 +5823,6 @@
|
||||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.15.0",
|
"acorn": "^8.15.0",
|
||||||
|
|
@ -6107,7 +6100,6 @@
|
||||||
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
|
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.8",
|
"@types/estree": "^1.0.8",
|
||||||
|
|
@ -6157,7 +6149,6 @@
|
||||||
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
|
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discoveryjs/json-ext": "^0.5.0",
|
"@discoveryjs/json-ext": "^0.5.0",
|
||||||
"@webpack-cli/configtest": "^2.1.1",
|
"@webpack-cli/configtest": "^2.1.1",
|
||||||
|
|
|
||||||
|
|
@ -129,9 +129,11 @@ class TemplateArgs(StrictAttributeDict):
|
||||||
|
|
||||||
class BaseAppView(object):
|
class BaseAppView(object):
|
||||||
DONT_CHECKOUT_VIEWS = ["channelstream_connect", "ops_ping"]
|
DONT_CHECKOUT_VIEWS = ["channelstream_connect", "ops_ping"]
|
||||||
EXTRA_VIEWS_TO_IGNORE = ["login", "register", "logout"]
|
EXTRA_VIEWS_TO_IGNORE = [
|
||||||
SETUP_2FA_VIEW = "setup_2fa"
|
"login", "register", "logout",
|
||||||
VERIFY_2FA_VIEW = "check_2fa"
|
"login_otp_verify",
|
||||||
|
"sudo_otp_verify", "resend_otp",
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, context, request):
|
def __init__(self, context, request):
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
@ -147,70 +149,6 @@ class BaseAppView(object):
|
||||||
self._rhodecode_user = request.user # auth user
|
self._rhodecode_user = request.user # auth user
|
||||||
self._rhodecode_db_user = self._rhodecode_user.get_instance()
|
self._rhodecode_db_user = self._rhodecode_user.get_instance()
|
||||||
self.user_data = self._rhodecode_db_user.user_data if self._rhodecode_db_user else {}
|
self.user_data = self._rhodecode_db_user.user_data if self._rhodecode_db_user else {}
|
||||||
self._maybe_needs_password_change(request.matched_route.name, self._rhodecode_db_user)
|
|
||||||
self._maybe_needs_2fa_configuration(request.matched_route.name, self._rhodecode_db_user)
|
|
||||||
self._maybe_needs_2fa_check(request.matched_route.name, self._rhodecode_db_user)
|
|
||||||
|
|
||||||
def _maybe_needs_password_change(self, view_name, user_obj):
|
|
||||||
if view_name in self.DONT_CHECKOUT_VIEWS:
|
|
||||||
return
|
|
||||||
|
|
||||||
log.debug("Checking if user %s needs password change on view %s", user_obj, view_name)
|
|
||||||
|
|
||||||
skip_user_views = [
|
|
||||||
"logout",
|
|
||||||
"login",
|
|
||||||
"check_2fa",
|
|
||||||
"my_account_password",
|
|
||||||
"my_account_password_update",
|
|
||||||
]
|
|
||||||
|
|
||||||
if not user_obj:
|
|
||||||
return
|
|
||||||
|
|
||||||
if user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]:
|
|
||||||
return
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
should_change = self.user_data.get("force_password_change")
|
|
||||||
change_after = safe_int(should_change) or 0
|
|
||||||
if should_change and now > change_after:
|
|
||||||
log.debug("User %s requires password change", user_obj)
|
|
||||||
h.flash(
|
|
||||||
"You are required to change your password",
|
|
||||||
"warning",
|
|
||||||
ignore_duplicate=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if view_name not in skip_user_views:
|
|
||||||
raise HTTPFound(self.request.route_path("my_account_password"))
|
|
||||||
|
|
||||||
def _maybe_needs_2fa_configuration(self, view_name, user_obj):
|
|
||||||
if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not user_obj:
|
|
||||||
return
|
|
||||||
|
|
||||||
if user_obj.needs_2fa_configure and view_name != self.SETUP_2FA_VIEW:
|
|
||||||
h.flash(
|
|
||||||
"You are required to configure 2FA",
|
|
||||||
"warning",
|
|
||||||
ignore_duplicate=False,
|
|
||||||
)
|
|
||||||
# Special case for users created "on the fly" (ldap case for new user)
|
|
||||||
user_obj.check_2fa_required = False
|
|
||||||
raise HTTPFound(self.request.route_path(self.SETUP_2FA_VIEW))
|
|
||||||
|
|
||||||
def _maybe_needs_2fa_check(self, view_name, user_obj):
|
|
||||||
if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not user_obj:
|
|
||||||
return
|
|
||||||
|
|
||||||
if user_obj.check_2fa_required and view_name != self.VERIFY_2FA_VIEW:
|
|
||||||
raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW))
|
|
||||||
|
|
||||||
def _log_creation_exception(self, e, repo_name):
|
def _log_creation_exception(self, e, repo_name):
|
||||||
_ = self.request.translate
|
_ = self.request.translate
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ from rhodecode.apps._base import BaseAppView, DataGridAppView
|
||||||
from rhodecode.lib.auth import LoginRequired, CSRFRequired, NotAnonymous, HasPermissionAny, HasRepoGroupPermissionAny
|
from rhodecode.lib.auth import LoginRequired, CSRFRequired, NotAnonymous, HasPermissionAny, HasRepoGroupPermissionAny
|
||||||
from rhodecode.lib import helpers as h, audit_logger
|
from rhodecode.lib import helpers as h, audit_logger
|
||||||
from rhodecode.lib.str_utils import safe_int, safe_str
|
from rhodecode.lib.str_utils import safe_int, safe_str
|
||||||
|
from rhodecode.lib.utils import repo_name_slug
|
||||||
from rhodecode.model.forms import RepoGroupForm
|
from rhodecode.model.forms import RepoGroupForm
|
||||||
from rhodecode.model.permission import PermissionModel
|
from rhodecode.model.permission import PermissionModel
|
||||||
from rhodecode.model.repo_group import RepoGroupModel
|
from rhodecode.model.repo_group import RepoGroupModel
|
||||||
|
|
@ -283,18 +284,23 @@ class AdminRepoGroupsView(BaseAppView, DataGridAppView):
|
||||||
can_create = self._can_create_repo_group(parent_group_id)
|
can_create = self._can_create_repo_group(parent_group_id)
|
||||||
|
|
||||||
self._load_form_data(c)
|
self._load_form_data(c)
|
||||||
# permissions for can create group based on parent_id are checked
|
|
||||||
# here in the Form
|
raw_group_name = self.request.POST.get("group_name", "").strip()
|
||||||
|
|
||||||
|
# Path-based creation: if the name contains "/" we create
|
||||||
|
# intermediate groups automatically (e.g. "org/team/project")
|
||||||
|
if "/" in raw_group_name:
|
||||||
|
return self._create_repo_group_path(c, raw_group_name, parent_group_id)
|
||||||
|
|
||||||
|
# Standard single-group creation via formencode
|
||||||
available_groups = list(map(lambda k: safe_str(k[0]), c.repo_groups))
|
available_groups = list(map(lambda k: safe_str(k[0]), c.repo_groups))
|
||||||
repo_group_form = RepoGroupForm(
|
repo_group_form = RepoGroupForm(
|
||||||
self.request.translate, available_groups=available_groups, can_create_in_root=can_create
|
self.request.translate, available_groups=available_groups, can_create_in_root=can_create
|
||||||
)()
|
)()
|
||||||
|
|
||||||
repo_group_name = self.request.POST.get("group_name")
|
|
||||||
try:
|
try:
|
||||||
owner = self._rhodecode_user
|
owner = self._rhodecode_user
|
||||||
form_result = repo_group_form.to_python(dict(self.request.POST))
|
form_result = repo_group_form.to_python(dict(self.request.POST))
|
||||||
copy_permissions = form_result.get("group_copy_permissions")
|
|
||||||
repo_group = RepoGroupModel().create(
|
repo_group = RepoGroupModel().create(
|
||||||
group_name=form_result["group_name_full"],
|
group_name=form_result["group_name_full"],
|
||||||
group_description=form_result["group_description"],
|
group_description=form_result["group_description"],
|
||||||
|
|
@ -332,9 +338,97 @@ class AdminRepoGroupsView(BaseAppView, DataGridAppView):
|
||||||
return Response(html)
|
return Response(html)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Exception during creation of repository group")
|
log.exception("Exception during creation of repository group")
|
||||||
h.flash(_("Error occurred during creation of repository group %s") % repo_group_name, category="error")
|
h.flash(_("Error occurred during creation of repository group %s") % raw_group_name, category="error")
|
||||||
raise HTTPFound(h.route_path("home"))
|
raise HTTPFound(h.route_path("home"))
|
||||||
|
|
||||||
PermissionModel().trigger_permission_flush()
|
PermissionModel().trigger_permission_flush()
|
||||||
|
|
||||||
raise HTTPFound(h.route_path("repo_group_home", repo_group_name=form_result["group_name_full"]))
|
raise HTTPFound(h.route_path("repo_group_home", repo_group_name=form_result["group_name_full"]))
|
||||||
|
|
||||||
|
def _create_repo_group_path(self, c, path_input, parent_group_id):
|
||||||
|
"""
|
||||||
|
Create a chain of nested repo groups from a slash-separated path.
|
||||||
|
E.g. "org/team/project" creates org, org/team, org/team/project
|
||||||
|
(skipping any that already exist).
|
||||||
|
"""
|
||||||
|
_ = self.request.translate
|
||||||
|
owner = self._rhodecode_user
|
||||||
|
|
||||||
|
# Resolve parent group prefix
|
||||||
|
parent_prefix = ""
|
||||||
|
if parent_group_id and parent_group_id != -1:
|
||||||
|
parent_group = RepoGroup.get(parent_group_id)
|
||||||
|
if parent_group:
|
||||||
|
parent_prefix = parent_group.group_name + "/"
|
||||||
|
|
||||||
|
# Split path into segments and slugify each one
|
||||||
|
segments = [repo_name_slug(s.strip()) for s in path_input.split("/") if s.strip()]
|
||||||
|
if not segments:
|
||||||
|
h.flash(_("Invalid group path"), category="error")
|
||||||
|
raise HTTPFound(h.route_path("repo_group_new"))
|
||||||
|
|
||||||
|
copy_permissions = self.request.POST.get("group_copy_permissions") == "True"
|
||||||
|
description = self.request.POST.get("group_description", "")
|
||||||
|
created_groups = []
|
||||||
|
final_group_name = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
for i, segment in enumerate(segments):
|
||||||
|
full_name = parent_prefix + "/".join(segments[: i + 1])
|
||||||
|
|
||||||
|
# Skip if group already exists
|
||||||
|
existing = RepoGroup.get_by_group_name(full_name)
|
||||||
|
if existing:
|
||||||
|
log.debug("Group %s already exists, skipping", full_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Only apply description to the final group
|
||||||
|
grp_description = description if i == len(segments) - 1 else segment
|
||||||
|
|
||||||
|
repo_group = RepoGroupModel().create(
|
||||||
|
group_name=full_name,
|
||||||
|
group_description=grp_description,
|
||||||
|
owner=owner.user_id,
|
||||||
|
copy_permissions=copy_permissions,
|
||||||
|
)
|
||||||
|
Session().flush()
|
||||||
|
|
||||||
|
audit_logger.store_web(
|
||||||
|
"repo_group.create",
|
||||||
|
action_data={"data": repo_group.get_api_data()},
|
||||||
|
user=self._rhodecode_user,
|
||||||
|
)
|
||||||
|
created_groups.append(full_name)
|
||||||
|
final_group_name = full_name
|
||||||
|
|
||||||
|
Session().commit()
|
||||||
|
|
||||||
|
if not final_group_name:
|
||||||
|
# All groups already existed; redirect to the deepest one
|
||||||
|
final_group_name = parent_prefix + "/".join(segments)
|
||||||
|
|
||||||
|
if len(created_groups) == 1:
|
||||||
|
msg = _("Created repository group %s") % h.link_to(
|
||||||
|
created_groups[0],
|
||||||
|
h.route_path("repo_group_home", repo_group_name=created_groups[0]),
|
||||||
|
)
|
||||||
|
elif created_groups:
|
||||||
|
msg = _("Created %d repository groups: %s") % (
|
||||||
|
len(created_groups),
|
||||||
|
", ".join(created_groups),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg = _("All groups already exist")
|
||||||
|
|
||||||
|
h.flash(h.literal(msg), category="success")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
log.exception("Exception during path-based repository group creation")
|
||||||
|
h.flash(
|
||||||
|
_("Error creating repository group path: %s") % path_input,
|
||||||
|
category="error",
|
||||||
|
)
|
||||||
|
raise HTTPFound(h.route_path("home"))
|
||||||
|
|
||||||
|
PermissionModel().trigger_permission_flush()
|
||||||
|
raise HTTPFound(h.route_path("repo_group_home", repo_group_name=final_group_name))
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ from rhodecode.lib.exceptions import (
|
||||||
)
|
)
|
||||||
from rhodecode.lib import ext_json
|
from rhodecode.lib import ext_json
|
||||||
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, CSRFRequired
|
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, CSRFRequired
|
||||||
|
from rhodecode.lib.auth_otp import SudoOTPRequired
|
||||||
from rhodecode.lib import helpers as h
|
from rhodecode.lib import helpers as h
|
||||||
from rhodecode.lib.helpers import SqlPage
|
from rhodecode.lib.helpers import SqlPage
|
||||||
from rhodecode.lib.utils2 import safe_int, safe_str, AttributeDict
|
from rhodecode.lib.utils2 import safe_int, safe_str, AttributeDict
|
||||||
|
|
@ -360,6 +361,7 @@ class UsersView(UserAppView):
|
||||||
@LoginRequired()
|
@LoginRequired()
|
||||||
@HasPermissionAllDecorator("hg.admin")
|
@HasPermissionAllDecorator("hg.admin")
|
||||||
@CSRFRequired()
|
@CSRFRequired()
|
||||||
|
@SudoOTPRequired
|
||||||
def user_delete(self):
|
def user_delete(self):
|
||||||
_ = self.request.translate
|
_ = self.request.translate
|
||||||
c = self.load_default_context()
|
c = self.load_default_context()
|
||||||
|
|
|
||||||
|
|
@ -35,60 +35,61 @@ def includeme(config):
|
||||||
renderer="rhodecode:templates/login.mako",
|
renderer="rhodecode:templates/login.mako",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
config.add_route(name="login_otp_verify", pattern=ADMIN_PREFIX + "/login/verify")
|
||||||
|
config.add_view(
|
||||||
|
LoginView,
|
||||||
|
attr="login_otp_verify",
|
||||||
|
route_name="login_otp_verify",
|
||||||
|
request_method="GET",
|
||||||
|
renderer="rhodecode:templates/otp_verify.mako",
|
||||||
|
)
|
||||||
|
config.add_view(
|
||||||
|
LoginView,
|
||||||
|
attr="login_otp_verify_post",
|
||||||
|
route_name="login_otp_verify",
|
||||||
|
request_method="POST",
|
||||||
|
renderer="rhodecode:templates/otp_verify.mako",
|
||||||
|
)
|
||||||
|
|
||||||
|
config.add_route(name="resend_otp", pattern=ADMIN_PREFIX + "/otp/resend")
|
||||||
|
config.add_view(
|
||||||
|
LoginView,
|
||||||
|
attr="resend_otp",
|
||||||
|
route_name="resend_otp",
|
||||||
|
request_method="POST",
|
||||||
|
renderer="json",
|
||||||
|
)
|
||||||
|
|
||||||
config.add_route(name="logout", pattern=ADMIN_PREFIX + "/logout")
|
config.add_route(name="logout", pattern=ADMIN_PREFIX + "/logout")
|
||||||
config.add_view(LoginView, attr="logout", route_name="logout", request_method="POST")
|
config.add_view(LoginView, attr="logout", route_name="logout", request_method="POST")
|
||||||
|
|
||||||
|
# Register redirects to login (unified flow)
|
||||||
config.add_route(name="register", pattern=ADMIN_PREFIX + "/register")
|
config.add_route(name="register", pattern=ADMIN_PREFIX + "/register")
|
||||||
config.add_view(
|
config.add_view(
|
||||||
LoginView,
|
LoginView,
|
||||||
attr="register",
|
attr="register",
|
||||||
route_name="register",
|
route_name="register",
|
||||||
request_method="GET",
|
request_method="GET",
|
||||||
renderer="rhodecode:templates/register.mako",
|
|
||||||
)
|
)
|
||||||
config.add_view(
|
config.add_view(
|
||||||
LoginView,
|
LoginView,
|
||||||
attr="register_post",
|
attr="register",
|
||||||
route_name="register",
|
route_name="register",
|
||||||
request_method="POST",
|
request_method="POST",
|
||||||
renderer="rhodecode:templates/register.mako",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
config.add_route(name="reset_password", pattern=ADMIN_PREFIX + "/password_reset")
|
config.add_route(name="sudo_otp_verify", pattern=ADMIN_PREFIX + "/sudo/verify")
|
||||||
config.add_view(
|
config.add_view(
|
||||||
LoginView,
|
LoginView,
|
||||||
attr="password_reset",
|
attr="sudo_otp_verify",
|
||||||
route_name="reset_password",
|
route_name="sudo_otp_verify",
|
||||||
request_method=("GET", "POST"),
|
|
||||||
renderer="rhodecode:templates/password_reset.mako",
|
|
||||||
)
|
|
||||||
|
|
||||||
config.add_route(name="reset_password_confirmation", pattern=ADMIN_PREFIX + "/password_reset_confirmation")
|
|
||||||
config.add_view(
|
|
||||||
LoginView, attr="password_reset_confirmation", route_name="reset_password_confirmation", request_method="GET"
|
|
||||||
)
|
|
||||||
|
|
||||||
config.add_route(name="setup_2fa", pattern=ADMIN_PREFIX + "/setup_2fa")
|
|
||||||
config.add_view(
|
|
||||||
LoginView,
|
|
||||||
attr="setup_2fa",
|
|
||||||
route_name="setup_2fa",
|
|
||||||
request_method=["GET", "POST"],
|
|
||||||
renderer="rhodecode:templates/configure_2fa.mako",
|
|
||||||
)
|
|
||||||
|
|
||||||
config.add_route(name="check_2fa", pattern=ADMIN_PREFIX + "/check_2fa")
|
|
||||||
config.add_view(
|
|
||||||
LoginView,
|
|
||||||
attr="verify_2fa",
|
|
||||||
route_name="check_2fa",
|
|
||||||
request_method="GET",
|
request_method="GET",
|
||||||
renderer="rhodecode:templates/verify_2fa.mako",
|
renderer="rhodecode:templates/sudo_otp_verify.mako",
|
||||||
)
|
)
|
||||||
config.add_view(
|
config.add_view(
|
||||||
LoginView,
|
LoginView,
|
||||||
attr="verify_2fa",
|
attr="sudo_otp_verify_post",
|
||||||
route_name="check_2fa",
|
route_name="sudo_otp_verify",
|
||||||
request_method="POST",
|
request_method="POST",
|
||||||
renderer="rhodecode:templates/verify_2fa.mako",
|
renderer="rhodecode:templates/sudo_otp_verify.mako",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@
|
||||||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import secrets
|
||||||
import time
|
import time
|
||||||
import pyotp
|
|
||||||
import qrcode
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
import formencode
|
import formencode
|
||||||
|
|
@ -26,17 +27,12 @@ import formencode.htmlfill
|
||||||
import logging
|
import logging
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import requests
|
import requests
|
||||||
from io import BytesIO
|
|
||||||
from base64 import b64encode
|
|
||||||
|
|
||||||
from pyramid.renderers import render
|
|
||||||
from pyramid.response import Response
|
|
||||||
from pyramid.httpexceptions import HTTPFound
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
import rhodecode
|
import rhodecode
|
||||||
from rhodecode.apps._base import BaseAppView
|
from rhodecode.apps._base import BaseAppView
|
||||||
from rhodecode.authentication.base import authenticate, HTTP_TYPE
|
from rhodecode.authentication.base import authenticate, HTTP_TYPE
|
||||||
from rhodecode.authentication.plugins import auth_rhodecode
|
|
||||||
from rhodecode.events import UserRegistered, trigger
|
from rhodecode.events import UserRegistered, trigger
|
||||||
from rhodecode.lib import helpers as h
|
from rhodecode.lib import helpers as h
|
||||||
from rhodecode.lib import audit_logger
|
from rhodecode.lib import audit_logger
|
||||||
|
|
@ -44,10 +40,9 @@ from rhodecode.lib.auth import AuthUser, HasPermissionAnyDecorator, CSRFRequired
|
||||||
from rhodecode.lib.base import get_ip_addr
|
from rhodecode.lib.base import get_ip_addr
|
||||||
from rhodecode.lib.exceptions import UserCreationError
|
from rhodecode.lib.exceptions import UserCreationError
|
||||||
from rhodecode.lib.utils2 import safe_str
|
from rhodecode.lib.utils2 import safe_str
|
||||||
from rhodecode.model.db import User, UserApiKeys
|
from rhodecode.model.db import User
|
||||||
from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm
|
from rhodecode.model.forms import LoginForm, OTPVerifyForm
|
||||||
from rhodecode.model.meta import Session
|
from rhodecode.model.meta import Session
|
||||||
from rhodecode.model.auth_token import AuthTokenModel
|
|
||||||
from rhodecode.model.settings import SettingsModel
|
from rhodecode.model.settings import SettingsModel
|
||||||
from rhodecode.model.user import UserModel
|
from rhodecode.model.user import UserModel
|
||||||
from rhodecode.translation import _
|
from rhodecode.translation import _
|
||||||
|
|
@ -84,15 +79,8 @@ def store_user_in_session(session, user_identifier, remember=False):
|
||||||
safe_cs["password"] = "****"
|
safe_cs["password"] = "****"
|
||||||
log.info("user %s is now authenticated and stored in session, session attrs %s", user_identifier, safe_cs)
|
log.info("user %s is now authenticated and stored in session, session attrs %s", user_identifier, safe_cs)
|
||||||
|
|
||||||
# dumps session attrs back to cookie
|
# Cookie is set automatically by Pyramid's session response callback
|
||||||
if hasattr(session, '_update_cookie_out'):
|
return None
|
||||||
session._update_cookie_out()
|
|
||||||
# we set new cookie
|
|
||||||
headers = None
|
|
||||||
if session.request["set_cookie"]:
|
|
||||||
# send set-cookie headers back to response to update cookie
|
|
||||||
headers = [("Set-Cookie", session.request["cookie_out"])]
|
|
||||||
return headers
|
|
||||||
|
|
||||||
|
|
||||||
def get_came_from(request):
|
def get_came_from(request):
|
||||||
|
|
@ -114,6 +102,21 @@ def get_came_from(request):
|
||||||
return came_from or default_came_from
|
return came_from or default_came_from
|
||||||
|
|
||||||
|
|
||||||
|
def _derive_username_from_email(email):
|
||||||
|
local_part = email.split("@")[0]
|
||||||
|
# sanitize: keep only alphanumeric, dash, underscore, dot
|
||||||
|
username = re.sub(r'[^a-zA-Z0-9._-]', '_', local_part).lower()
|
||||||
|
if not username:
|
||||||
|
username = 'user'
|
||||||
|
# ensure unique
|
||||||
|
base = username
|
||||||
|
suffix = 0
|
||||||
|
while User.get_by_username(username):
|
||||||
|
suffix += 1
|
||||||
|
username = f'{base}_{suffix}'
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
class LoginView(BaseAppView):
|
class LoginView(BaseAppView):
|
||||||
def load_default_context(self):
|
def load_default_context(self):
|
||||||
c = self._get_local_tmpl_context()
|
c = self._get_local_tmpl_context()
|
||||||
|
|
@ -144,6 +147,28 @@ class LoginView(BaseAppView):
|
||||||
|
|
||||||
return captcha_status, captcha_message
|
return captcha_status, captcha_message
|
||||||
|
|
||||||
|
def _send_otp_email(self, email, code, purpose):
|
||||||
|
from rhodecode.lib.celerylib import tasks, run_task
|
||||||
|
from rhodecode.model.notification import EmailNotificationModel
|
||||||
|
|
||||||
|
import rhodecode
|
||||||
|
smtp_server = rhodecode.CONFIG.get('smtp_server')
|
||||||
|
log_code = code if not smtp_server else 'X' * len(code)
|
||||||
|
log.info('[OTP] code=%s email=%s purpose=%s', log_code, email, purpose)
|
||||||
|
|
||||||
|
email_kwargs = {
|
||||||
|
'otp_code': code,
|
||||||
|
'purpose': purpose,
|
||||||
|
}
|
||||||
|
|
||||||
|
(subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
|
||||||
|
EmailNotificationModel.TYPE_OTP_CODE, **email_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
run_task(tasks.send_email, [email], subject, email_body_plaintext, email_body)
|
||||||
|
|
||||||
|
# ---- Login Step 1: Email ----
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
c = self.load_default_context()
|
c = self.load_default_context()
|
||||||
auth_user = self._rhodecode_user
|
auth_user = self._rhodecode_user
|
||||||
|
|
@ -171,46 +196,255 @@ class LoginView(BaseAppView):
|
||||||
login_form = LoginForm(self.request.translate)()
|
login_form = LoginForm(self.request.translate)()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.session.invalidate()
|
|
||||||
form_result = login_form.to_python(self.request.POST)
|
form_result = login_form.to_python(self.request.POST)
|
||||||
# form checks for username/password, now we're authenticated
|
email = form_result["email"]
|
||||||
username = form_result["username"]
|
remember = form_result["remember"]
|
||||||
if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa:
|
|
||||||
user.check_2fa_required = True
|
|
||||||
|
|
||||||
headers = store_user_in_session(self.session, user_identifier=username, remember=form_result["remember"])
|
# Throttle to prevent enumeration
|
||||||
log.debug('Redirecting to "%s" after login.', c.came_from)
|
time.sleep(1)
|
||||||
|
|
||||||
audit_user = audit_logger.UserWrap(
|
user = User.get_by_email(email)
|
||||||
username=self.request.POST.get("username"), ip_addr=self.request.remote_addr
|
if user and user.active:
|
||||||
)
|
# Existing active user — generate OTP on user object
|
||||||
action_data = {"user_agent": self.request.user_agent}
|
code = user.generate_otp("login")
|
||||||
audit_logger.store_web("user.login.success", action_data=action_data, user=audit_user, commit=True)
|
Session().commit()
|
||||||
|
self._send_otp_email(email, code, "login")
|
||||||
|
self.session["otp_is_new_user"] = False
|
||||||
|
elif user and not user.active:
|
||||||
|
# Inactive user — don't send OTP but still redirect (anti-enumeration)
|
||||||
|
log.debug("login OTP request for inactive email %s", email)
|
||||||
|
self.session["otp_is_new_user"] = False
|
||||||
|
else:
|
||||||
|
# Unknown email — auto-register if allowed
|
||||||
|
default_user = User.get_default_user()
|
||||||
|
perms = default_user.AuthUser().permissions["global"]
|
||||||
|
registration_allowed = (
|
||||||
|
"hg.register.auto_activate" in perms
|
||||||
|
or "hg.register.manual_activate" in perms
|
||||||
|
)
|
||||||
|
|
||||||
|
if registration_allowed:
|
||||||
|
# Generate session-based OTP for new user registration
|
||||||
|
code = ''.join(str(secrets.randbelow(10)) for _ in range(6))
|
||||||
|
code_hash = hashlib.sha256(code.encode('utf-8')).hexdigest()
|
||||||
|
self.session["otp_hash"] = code_hash
|
||||||
|
self.session["otp_created_at"] = time.time()
|
||||||
|
self.session["otp_attempts"] = 0
|
||||||
|
self.session["otp_is_new_user"] = True
|
||||||
|
self._send_otp_email(email, code, "login")
|
||||||
|
else:
|
||||||
|
log.debug("login OTP request for unknown email %s, registration disabled", email)
|
||||||
|
self.session["otp_is_new_user"] = False
|
||||||
|
|
||||||
|
# Always store in session and redirect (prevent email enumeration)
|
||||||
|
self.session["otp_email"] = email
|
||||||
|
self.session["otp_remember"] = remember
|
||||||
|
self.session["otp_came_from"] = c.came_from
|
||||||
|
if hasattr(self.session, 'save'):
|
||||||
|
self.session.save()
|
||||||
|
|
||||||
|
raise HTTPFound(self.request.route_path("login_otp_verify"))
|
||||||
|
|
||||||
raise HTTPFound(c.came_from, headers=headers)
|
|
||||||
except formencode.Invalid as errors:
|
except formencode.Invalid as errors:
|
||||||
defaults = errors.value
|
defaults = errors.value
|
||||||
# remove password from filling in form again
|
|
||||||
defaults.pop("password", None)
|
|
||||||
render_ctx = {
|
render_ctx = {
|
||||||
"errors": errors.error_dict,
|
"errors": errors.error_dict,
|
||||||
"defaults": defaults,
|
"defaults": defaults,
|
||||||
}
|
}
|
||||||
|
|
||||||
audit_user = audit_logger.UserWrap(
|
|
||||||
username=self.request.POST.get("username"), ip_addr=self.request.remote_addr
|
|
||||||
)
|
|
||||||
action_data = {"user_agent": self.request.user_agent}
|
|
||||||
audit_logger.store_web("user.login.failure", action_data=action_data, user=audit_user, commit=True)
|
|
||||||
return self._get_template_context(c, **render_ctx)
|
return self._get_template_context(c, **render_ctx)
|
||||||
|
|
||||||
except UserCreationError as e:
|
# ---- Login Step 2: OTP Verify ----
|
||||||
# headers auth or other auth functions that create users on
|
|
||||||
# the fly can throw this exception signaling that there's issue
|
def login_otp_verify(self):
|
||||||
# with user creation, explanation should be provided in
|
c = self.load_default_context()
|
||||||
# Exception itself
|
email = self.session.get("otp_email")
|
||||||
h.flash(e, category="error")
|
if not email:
|
||||||
return self._get_template_context(c)
|
raise HTTPFound(self.request.route_path("login"))
|
||||||
|
return self._get_template_context(c,
|
||||||
|
verify_route="login_otp_verify",
|
||||||
|
otp_digits=6,
|
||||||
|
purpose="login",
|
||||||
|
otp_email=email,
|
||||||
|
)
|
||||||
|
|
||||||
|
def login_otp_verify_post(self):
|
||||||
|
c = self.load_default_context()
|
||||||
|
email = self.session.get("otp_email")
|
||||||
|
is_new_user = self.session.get("otp_is_new_user", False)
|
||||||
|
log.debug("OTP verify: email=%s is_new_user=%s session_keys=%s",
|
||||||
|
email, is_new_user, list(self.session.keys()))
|
||||||
|
if not email:
|
||||||
|
raise HTTPFound(self.request.route_path("login"))
|
||||||
|
|
||||||
|
otp_form = OTPVerifyForm(self.request.translate, digits=6)()
|
||||||
|
|
||||||
|
try:
|
||||||
|
form_result = otp_form.to_python(self.request.POST)
|
||||||
|
code = form_result["otp"]
|
||||||
|
|
||||||
|
if is_new_user:
|
||||||
|
self._verify_new_user_otp(email, code, form_result)
|
||||||
|
else:
|
||||||
|
self._verify_existing_user_otp(email, code, form_result)
|
||||||
|
|
||||||
|
except formencode.Invalid as errors:
|
||||||
|
defaults = errors.value
|
||||||
|
render_ctx = {
|
||||||
|
"errors": errors.error_dict,
|
||||||
|
"defaults": defaults,
|
||||||
|
"verify_route": "login_otp_verify",
|
||||||
|
"otp_digits": 6,
|
||||||
|
"purpose": "login",
|
||||||
|
"otp_email": email,
|
||||||
|
}
|
||||||
|
return self._get_template_context(c, **render_ctx)
|
||||||
|
|
||||||
|
def _verify_existing_user_otp(self, email, code, form_result):
|
||||||
|
user = User.get_by_email(email)
|
||||||
|
if not user or not user.verify_otp(code, "login"):
|
||||||
|
Session().commit()
|
||||||
|
error_msg = _("Invalid or expired code. Please try again.")
|
||||||
|
raise formencode.Invalid(error_msg, form_result, None,
|
||||||
|
error_dict={"otp": error_msg})
|
||||||
|
|
||||||
|
Session().commit()
|
||||||
|
|
||||||
|
remember = self.session.get("otp_remember", False)
|
||||||
|
came_from = self.session.get("otp_came_from", h.route_path("home"))
|
||||||
|
|
||||||
|
# Clear OTP session data
|
||||||
|
self._clear_otp_session()
|
||||||
|
|
||||||
|
headers = store_user_in_session(self.session, user_identifier=email, remember=remember)
|
||||||
|
|
||||||
|
audit_user = audit_logger.UserWrap(
|
||||||
|
username=user.username, ip_addr=self.request.remote_addr
|
||||||
|
)
|
||||||
|
action_data = {"user_agent": self.request.user_agent}
|
||||||
|
audit_logger.store_web("user.login.success", action_data=action_data, user=audit_user, commit=True)
|
||||||
|
|
||||||
|
raise HTTPFound(came_from, headers=headers)
|
||||||
|
|
||||||
|
def _verify_new_user_otp(self, email, code, form_result):
|
||||||
|
from rhodecode.authentication.plugins import auth_rhodecode
|
||||||
|
|
||||||
|
stored_hash = self.session.get("otp_hash")
|
||||||
|
created_at = self.session.get("otp_created_at", 0)
|
||||||
|
attempts = self.session.get("otp_attempts", 0)
|
||||||
|
|
||||||
|
if attempts >= 5:
|
||||||
|
self._clear_otp_session()
|
||||||
|
h.flash(_("Too many attempts. Please start over."), category="error")
|
||||||
|
raise HTTPFound(self.request.route_path("login"))
|
||||||
|
|
||||||
|
if (time.time() - created_at) > 300:
|
||||||
|
self._clear_otp_session()
|
||||||
|
h.flash(_("Code expired. Please start over."), category="error")
|
||||||
|
raise HTTPFound(self.request.route_path("login"))
|
||||||
|
|
||||||
|
self.session["otp_attempts"] = attempts + 1
|
||||||
|
if hasattr(self.session, 'save'):
|
||||||
|
self.session.save()
|
||||||
|
|
||||||
|
code_hash = hashlib.sha256(code.encode('utf-8')).hexdigest()
|
||||||
|
if code_hash != stored_hash:
|
||||||
|
error_msg = _("Invalid code. Please try again.")
|
||||||
|
raise formencode.Invalid(error_msg, form_result, None,
|
||||||
|
error_dict={"otp": error_msg})
|
||||||
|
|
||||||
|
# OTP verified — create the user account
|
||||||
|
auto_active = "hg.register.auto_activate" in User.get_default_user().AuthUser().permissions["global"]
|
||||||
|
username = _derive_username_from_email(email)
|
||||||
|
|
||||||
|
# First real user becomes admin (bootstrap)
|
||||||
|
system_users = {User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER}
|
||||||
|
real_user_count = User.query().filter(~User.username.in_(system_users)).count()
|
||||||
|
is_first_user = real_user_count == 0
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
"username": username,
|
||||||
|
"password": secrets.token_hex(32),
|
||||||
|
"email": email,
|
||||||
|
"firstname": "",
|
||||||
|
"lastname": "",
|
||||||
|
"active": True if is_first_user else auto_active,
|
||||||
|
"admin": is_first_user,
|
||||||
|
}
|
||||||
|
|
||||||
|
extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
|
||||||
|
extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
|
||||||
|
|
||||||
|
new_user = UserModel().create_registration(form_data, extern_name=extern_name, extern_type=extern_type)
|
||||||
|
|
||||||
|
action_data = {"data": new_user.get_api_data(), "user_agent": self.request.user_agent}
|
||||||
|
audit_user = audit_logger.UserWrap(
|
||||||
|
username=new_user.username, user_id=new_user.user_id, ip_addr=self.request.remote_addr
|
||||||
|
)
|
||||||
|
audit_logger.store_web("user.register", action_data=action_data, user=audit_user)
|
||||||
|
|
||||||
|
event = UserRegistered(user=new_user, session=self.session)
|
||||||
|
trigger(event)
|
||||||
|
Session().commit()
|
||||||
|
|
||||||
|
# Clean up OTP session data
|
||||||
|
self._clear_otp_session()
|
||||||
|
|
||||||
|
if is_first_user or auto_active:
|
||||||
|
headers = store_user_in_session(self.session, user_identifier=email, remember=False)
|
||||||
|
h.flash(_("Your account has been created. Welcome!"), category="success")
|
||||||
|
raise HTTPFound(h.route_path("home"), headers=headers)
|
||||||
|
else:
|
||||||
|
h.flash(_("Your account has been created and awaits admin approval."), category="success")
|
||||||
|
raise HTTPFound(self.request.route_path("login"))
|
||||||
|
|
||||||
|
def _clear_otp_session(self):
|
||||||
|
for key in list(self.session.keys()):
|
||||||
|
if key.startswith("otp_"):
|
||||||
|
self.session.pop(key, None)
|
||||||
|
if hasattr(self.session, 'save'):
|
||||||
|
self.session.save()
|
||||||
|
|
||||||
|
# ---- Resend OTP ----
|
||||||
|
|
||||||
|
def resend_otp(self):
|
||||||
|
email = self.session.get("otp_email")
|
||||||
|
if not email:
|
||||||
|
return {"status": "error", "message": "No pending verification."}
|
||||||
|
|
||||||
|
# Rate limit: one per 60s
|
||||||
|
last_sent = self.session.get("otp_last_sent", 0)
|
||||||
|
if time.time() - last_sent < 60:
|
||||||
|
return {"status": "error", "message": "Please wait before requesting a new code."}
|
||||||
|
|
||||||
|
is_new_user = self.session.get("otp_is_new_user", False)
|
||||||
|
|
||||||
|
if is_new_user:
|
||||||
|
# New user: regenerate OTP stored in session
|
||||||
|
code = ''.join(str(secrets.randbelow(10)) for _ in range(6))
|
||||||
|
code_hash = hashlib.sha256(code.encode('utf-8')).hexdigest()
|
||||||
|
self.session["otp_hash"] = code_hash
|
||||||
|
self.session["otp_created_at"] = time.time()
|
||||||
|
self.session["otp_attempts"] = 0
|
||||||
|
else:
|
||||||
|
# Existing user: regenerate on the user object
|
||||||
|
user = User.get_by_email(email)
|
||||||
|
if user and user.active:
|
||||||
|
code = user.generate_otp("login")
|
||||||
|
Session().commit()
|
||||||
|
else:
|
||||||
|
# Fake success to prevent enumeration
|
||||||
|
self.session["otp_last_sent"] = time.time()
|
||||||
|
if hasattr(self.session, 'save'):
|
||||||
|
self.session.save()
|
||||||
|
return {"status": "ok", "message": "Code sent."}
|
||||||
|
|
||||||
|
self._send_otp_email(email, code, "verification")
|
||||||
|
self.session["otp_last_sent"] = time.time()
|
||||||
|
if hasattr(self.session, 'save'):
|
||||||
|
self.session.save()
|
||||||
|
return {"status": "ok", "message": "Code sent."}
|
||||||
|
|
||||||
|
# ---- Logout ----
|
||||||
|
|
||||||
@CSRFRequired()
|
@CSRFRequired()
|
||||||
def logout(self):
|
def logout(self):
|
||||||
|
|
@ -222,296 +456,51 @@ class LoginView(BaseAppView):
|
||||||
self.session.delete()
|
self.session.delete()
|
||||||
return HTTPFound(h.route_path("home"))
|
return HTTPFound(h.route_path("home"))
|
||||||
|
|
||||||
@HasPermissionAnyDecorator("hg.admin", "hg.register.auto_activate", "hg.register.manual_activate")
|
# ---- Registration (redirects to unified login flow) ----
|
||||||
|
|
||||||
def register(self, defaults=None, errors=None):
|
def register(self, defaults=None, errors=None):
|
||||||
|
raise HTTPFound(self.request.route_path("login"))
|
||||||
|
|
||||||
|
# ---- Sudo OTP ----
|
||||||
|
|
||||||
|
@LoginRequired()
|
||||||
|
@NotAnonymous()
|
||||||
|
def sudo_otp_verify(self):
|
||||||
c = self.load_default_context()
|
c = self.load_default_context()
|
||||||
defaults = defaults or {}
|
return self._get_template_context(c)
|
||||||
errors = errors or {}
|
|
||||||
|
|
||||||
settings = SettingsModel().get_all_settings()
|
@LoginRequired()
|
||||||
register_message = settings.get("rhodecode_register_message") or ""
|
@NotAnonymous()
|
||||||
captcha = self._get_captcha_data()
|
@CSRFRequired()
|
||||||
auto_active = "hg.register.auto_activate" in User.get_default_user().AuthUser().permissions["global"]
|
def sudo_otp_verify_post(self):
|
||||||
|
c = self.load_default_context()
|
||||||
|
user = self._rhodecode_db_user
|
||||||
|
|
||||||
render_ctx = self._get_template_context(c)
|
otp_form = OTPVerifyForm(self.request.translate, digits=8)()
|
||||||
render_ctx.update(
|
|
||||||
{
|
|
||||||
"defaults": defaults,
|
|
||||||
"errors": errors,
|
|
||||||
"auto_active": auto_active,
|
|
||||||
"captcha_active": captcha.active,
|
|
||||||
"captcha_public_key": captcha.public_key,
|
|
||||||
"register_message": register_message,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return render_ctx
|
|
||||||
|
|
||||||
@HasPermissionAnyDecorator("hg.admin", "hg.register.auto_activate", "hg.register.manual_activate")
|
|
||||||
def register_post(self):
|
|
||||||
from rhodecode.authentication.plugins import auth_rhodecode
|
|
||||||
|
|
||||||
self.load_default_context()
|
|
||||||
captcha = self._get_captcha_data()
|
|
||||||
auto_active = "hg.register.auto_activate" in User.get_default_user().AuthUser().permissions["global"]
|
|
||||||
|
|
||||||
extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
|
|
||||||
extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
|
|
||||||
|
|
||||||
register_form = RegisterForm(self.request.translate)()
|
|
||||||
try:
|
try:
|
||||||
form_result = register_form.to_python(self.request.POST)
|
form_result = otp_form.to_python(self.request.POST)
|
||||||
form_result["active"] = auto_active
|
code = form_result["otp"]
|
||||||
external_identity = self.request.POST.get("external_identity")
|
|
||||||
|
|
||||||
if external_identity:
|
if not user.verify_otp(code, "sudo"):
|
||||||
extern_name = external_identity
|
Session().commit()
|
||||||
extern_type = external_identity
|
error_msg = _("Invalid or expired code. Please try again.")
|
||||||
|
raise formencode.Invalid(error_msg, form_result, None,
|
||||||
|
error_dict={"otp": error_msg})
|
||||||
|
|
||||||
if captcha.active:
|
|
||||||
captcha_status, captcha_message = self.validate_captcha(captcha.private_key)
|
|
||||||
|
|
||||||
if not captcha_status:
|
|
||||||
_value = form_result
|
|
||||||
_msg = _("Bad captcha")
|
|
||||||
error_dict = {"recaptcha_field": captcha_message}
|
|
||||||
raise formencode.Invalid(_msg, _value, None, error_dict=error_dict)
|
|
||||||
|
|
||||||
new_user = UserModel().create_registration(form_result, extern_name=extern_name, extern_type=extern_type)
|
|
||||||
|
|
||||||
action_data = {"data": new_user.get_api_data(), "user_agent": self.request.user_agent}
|
|
||||||
|
|
||||||
if external_identity:
|
|
||||||
action_data["external_identity"] = external_identity
|
|
||||||
|
|
||||||
audit_user = audit_logger.UserWrap(
|
|
||||||
username=new_user.username, user_id=new_user.user_id, ip_addr=self.request.remote_addr
|
|
||||||
)
|
|
||||||
|
|
||||||
audit_logger.store_web("user.register", action_data=action_data, user=audit_user)
|
|
||||||
|
|
||||||
event = UserRegistered(user=new_user, session=self.session)
|
|
||||||
trigger(event)
|
|
||||||
h.flash(_("You have successfully registered with RhodeCode. You can log-in now."), category="success")
|
|
||||||
if external_identity:
|
|
||||||
h.flash(
|
|
||||||
_("Please use the {identity} button to log-in").format(identity=external_identity),
|
|
||||||
category="success",
|
|
||||||
)
|
|
||||||
Session().commit()
|
Session().commit()
|
||||||
|
|
||||||
redirect_ro = self.request.route_path("login")
|
self.session["sudo_otp_verified_at"] = time.time()
|
||||||
raise HTTPFound(redirect_ro)
|
next_url = self.session.pop("sudo_next_url", h.route_path("home"))
|
||||||
|
if hasattr(self.session, 'save'):
|
||||||
|
self.session.save()
|
||||||
|
|
||||||
|
raise HTTPFound(next_url)
|
||||||
|
|
||||||
except formencode.Invalid as errors:
|
except formencode.Invalid as errors:
|
||||||
errors.value.pop("password", None)
|
defaults = errors.value
|
||||||
errors.value.pop("password_confirmation", None)
|
render_ctx = {
|
||||||
return self.register(defaults=errors.value, errors=errors.error_dict)
|
"errors": errors.error_dict,
|
||||||
|
"defaults": defaults,
|
||||||
except UserCreationError as e:
|
}
|
||||||
# container auth or other auth functions that create users on
|
return self._get_template_context(c, **render_ctx)
|
||||||
# the fly can throw this exception signaling that there's issue
|
|
||||||
# with user creation, explanation should be provided in
|
|
||||||
# Exception itself
|
|
||||||
h.flash(e, category="error")
|
|
||||||
return self.register()
|
|
||||||
|
|
||||||
def password_reset(self):
|
|
||||||
c = self.load_default_context()
|
|
||||||
captcha = self._get_captcha_data()
|
|
||||||
|
|
||||||
template_context = {
|
|
||||||
"captcha_active": captcha.active,
|
|
||||||
"captcha_public_key": captcha.public_key,
|
|
||||||
"defaults": {},
|
|
||||||
"errors": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
# always send implicit message to prevent from discovery of
|
|
||||||
# matching emails
|
|
||||||
msg = _("If such email exists, a password reset link was sent to it.")
|
|
||||||
|
|
||||||
def default_response():
|
|
||||||
log.debug("faking response on invalid password reset")
|
|
||||||
# make this take 2s, to prevent brute forcing.
|
|
||||||
time.sleep(2)
|
|
||||||
h.flash(msg, category="success")
|
|
||||||
return HTTPFound(self.request.route_path("reset_password"))
|
|
||||||
|
|
||||||
if self.request.POST:
|
|
||||||
if h.HasPermissionAny("hg.password_reset.disabled")():
|
|
||||||
_email = self.request.POST.get("email", "")
|
|
||||||
log.error("Failed attempt to reset password for `%s`.", _email)
|
|
||||||
h.flash(_("Password reset has been disabled."), category="error")
|
|
||||||
return HTTPFound(self.request.route_path("reset_password"))
|
|
||||||
|
|
||||||
password_reset_form = PasswordResetForm(self.request.translate)()
|
|
||||||
description = "Generated token for password reset from {}".format(datetime.datetime.now().isoformat())
|
|
||||||
|
|
||||||
try:
|
|
||||||
form_result = password_reset_form.to_python(self.request.POST)
|
|
||||||
user_email = form_result["email"]
|
|
||||||
|
|
||||||
if captcha.active:
|
|
||||||
captcha_status, captcha_message = self.validate_captcha(captcha.private_key)
|
|
||||||
|
|
||||||
if not captcha_status:
|
|
||||||
_value = form_result
|
|
||||||
_msg = _("Bad captcha")
|
|
||||||
error_dict = {"recaptcha_field": captcha_message}
|
|
||||||
raise formencode.Invalid(_msg, _value, None, error_dict=error_dict)
|
|
||||||
|
|
||||||
# Generate reset URL and send mail.
|
|
||||||
user = User.get_by_email(user_email)
|
|
||||||
|
|
||||||
# only allow rhodecode based users to reset their password
|
|
||||||
# external auth shouldn't allow password reset
|
|
||||||
if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
|
|
||||||
log.warning(
|
|
||||||
"User %s with external type `%s` tried a password reset. This try was rejected",
|
|
||||||
user,
|
|
||||||
user.extern_type,
|
|
||||||
)
|
|
||||||
return default_response()
|
|
||||||
|
|
||||||
# generate password reset token that expires in 10 minutes
|
|
||||||
reset_token = UserModel().add_auth_token(
|
|
||||||
user=user,
|
|
||||||
lifetime_minutes=10,
|
|
||||||
role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
|
|
||||||
description=description,
|
|
||||||
)
|
|
||||||
Session().commit()
|
|
||||||
|
|
||||||
log.debug("Successfully created password recovery token")
|
|
||||||
password_reset_url = self.request.route_url(
|
|
||||||
"reset_password_confirmation", _query={"key": reset_token.api_key}
|
|
||||||
)
|
|
||||||
UserModel().reset_password_link(form_result, password_reset_url)
|
|
||||||
|
|
||||||
action_data = {"email": user_email, "user_agent": self.request.user_agent}
|
|
||||||
audit_logger.store_web(
|
|
||||||
"user.password.reset_request", action_data=action_data, user=self._rhodecode_user, commit=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return default_response()
|
|
||||||
|
|
||||||
except formencode.Invalid as errors:
|
|
||||||
template_context.update(
|
|
||||||
{
|
|
||||||
"defaults": errors.value,
|
|
||||||
"errors": errors.error_dict,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if not self.request.POST.get("email"):
|
|
||||||
# case of empty email, we want to report that
|
|
||||||
return self._get_template_context(c, **template_context)
|
|
||||||
|
|
||||||
if "recaptcha_field" in errors.error_dict:
|
|
||||||
# case of failed captcha
|
|
||||||
return self._get_template_context(c, **template_context)
|
|
||||||
|
|
||||||
return default_response()
|
|
||||||
|
|
||||||
return self._get_template_context(c, **template_context)
|
|
||||||
|
|
||||||
def password_reset_confirmation(self):
|
|
||||||
self.load_default_context()
|
|
||||||
|
|
||||||
if key := self.request.GET.get("key"):
|
|
||||||
# make this take 2s, to prevent brute forcing.
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
token = AuthTokenModel().get_auth_token(key)
|
|
||||||
|
|
||||||
# verify token is the correct role
|
|
||||||
if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
|
|
||||||
log.debug(
|
|
||||||
"Got token with role:%s expected is %s",
|
|
||||||
getattr(token, "role", "EMPTY_TOKEN"),
|
|
||||||
UserApiKeys.ROLE_PASSWORD_RESET,
|
|
||||||
)
|
|
||||||
h.flash(_("Given reset token is invalid"), category="error")
|
|
||||||
return HTTPFound(self.request.route_path("reset_password"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
owner = token.user
|
|
||||||
data = {"email": owner.email, "token": token.api_key}
|
|
||||||
UserModel().reset_password(data)
|
|
||||||
h.flash(
|
|
||||||
_("Your password reset was successful, a new password has been sent to your email"),
|
|
||||||
category="success",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
log.error(e)
|
|
||||||
return HTTPFound(self.request.route_path("reset_password"))
|
|
||||||
|
|
||||||
return HTTPFound(self.request.route_path("login"))
|
|
||||||
|
|
||||||
@LoginRequired()
|
|
||||||
@NotAnonymous()
|
|
||||||
def setup_2fa(self):
|
|
||||||
_ = self.request.translate
|
|
||||||
c = self.load_default_context()
|
|
||||||
user_instance = self._rhodecode_db_user
|
|
||||||
form = TOTPForm(_, user_instance)()
|
|
||||||
render_ctx = {}
|
|
||||||
if self.request.method == "POST":
|
|
||||||
post_items = dict(self.request.POST)
|
|
||||||
|
|
||||||
try:
|
|
||||||
form_details = form.to_python(post_items)
|
|
||||||
secret = form_details["secret_totp"]
|
|
||||||
|
|
||||||
user_instance.init_2fa_recovery_codes(persist=True, force=True)
|
|
||||||
user_instance.secret_2fa = secret
|
|
||||||
|
|
||||||
Session().commit()
|
|
||||||
raise HTTPFound(self.request.route_path("my_account_configure_2fa", _query={"show-recovery-codes": 1}))
|
|
||||||
except formencode.Invalid as errors:
|
|
||||||
defaults = errors.value
|
|
||||||
render_ctx = {
|
|
||||||
"errors": errors.error_dict,
|
|
||||||
"defaults": defaults,
|
|
||||||
}
|
|
||||||
|
|
||||||
# NOTE: here we DO NOT persist the secret 2FA, since this is only for setup, once a setup is completed
|
|
||||||
# only then we should persist it
|
|
||||||
secret = user_instance.init_secret_2fa(persist=False)
|
|
||||||
|
|
||||||
instance_name = rhodecode.ConfigGet().get_str("app.base_url", "rhodecode")
|
|
||||||
totp_name = f"{instance_name}:{self.request.user.username}"
|
|
||||||
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=5, border=4)
|
|
||||||
qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(name=totp_name))
|
|
||||||
qr.make(fit=True)
|
|
||||||
img = qr.make_image(fill_color="black", back_color="white")
|
|
||||||
buffered = BytesIO()
|
|
||||||
img.save(buffered)
|
|
||||||
return self._get_template_context(
|
|
||||||
c, qr=b64encode(buffered.getvalue()).decode("utf-8"), key=secret, totp_name=totp_name, **render_ctx
|
|
||||||
)
|
|
||||||
|
|
||||||
@LoginRequired()
|
|
||||||
@NotAnonymous()
|
|
||||||
def verify_2fa(self):
|
|
||||||
_ = self.request.translate
|
|
||||||
c = self.load_default_context()
|
|
||||||
render_ctx = {}
|
|
||||||
user_instance = self._rhodecode_db_user
|
|
||||||
totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
|
|
||||||
if self.request.method == "POST":
|
|
||||||
post_items = dict(self.request.POST)
|
|
||||||
# NOTE: inject secret, as it's a post configured saved item.
|
|
||||||
post_items["secret_totp"] = user_instance.secret_2fa
|
|
||||||
try:
|
|
||||||
totp_form.to_python(post_items)
|
|
||||||
user_instance.check_2fa_required = False
|
|
||||||
Session().commit()
|
|
||||||
raise HTTPFound(c.came_from)
|
|
||||||
except formencode.Invalid as errors:
|
|
||||||
defaults = errors.value
|
|
||||||
render_ctx = {
|
|
||||||
"errors": errors.error_dict,
|
|
||||||
"defaults": defaults,
|
|
||||||
}
|
|
||||||
return self._get_template_context(c, **render_ctx)
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ from rhodecode.model.db import (
|
||||||
RepoGroup,
|
RepoGroup,
|
||||||
ChangesetStatus,
|
ChangesetStatus,
|
||||||
)
|
)
|
||||||
from rhodecode.model.forms import TOTPForm
|
|
||||||
from rhodecode.model.meta import Session
|
from rhodecode.model.meta import Session
|
||||||
from rhodecode.model.pull_request import PullRequestModel
|
from rhodecode.model.pull_request import PullRequestModel
|
||||||
from rhodecode.model.user import UserModel
|
from rhodecode.model.user import UserModel
|
||||||
|
|
@ -257,26 +256,8 @@ class MyAccountView(BaseAppView, DataGridAppView):
|
||||||
@CSRFRequired()
|
@CSRFRequired()
|
||||||
def my_account_2fa_regenerate_recovery_codes(self):
|
def my_account_2fa_regenerate_recovery_codes(self):
|
||||||
_ = self.request.translate
|
_ = self.request.translate
|
||||||
c = self.load_default_context()
|
h.flash(_("2FA recovery codes are no longer used."), category="warning")
|
||||||
user_instance = c.auth_user.get_instance()
|
raise HTTPFound(self.request.route_path("my_account_configure_2fa"))
|
||||||
|
|
||||||
totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
|
|
||||||
|
|
||||||
post_items = dict(self.request.POST)
|
|
||||||
# NOTE: inject secret, as it's a post configured saved item.
|
|
||||||
post_items["secret_totp"] = user_instance.secret_2fa
|
|
||||||
try:
|
|
||||||
totp_form.to_python(post_items)
|
|
||||||
user_instance.regenerate_2fa_recovery_codes()
|
|
||||||
Session().commit()
|
|
||||||
except formencode.Invalid as errors:
|
|
||||||
h.flash(_("Failed to generate new recovery codes: {}").format(errors), category="error")
|
|
||||||
raise HTTPFound(self.request.route_path("my_account_configure_2fa"))
|
|
||||||
except Exception as e:
|
|
||||||
h.flash(_("Failed to generate new recovery codes: {}").format(e), category="error")
|
|
||||||
raise HTTPFound(self.request.route_path("my_account_configure_2fa"))
|
|
||||||
|
|
||||||
raise HTTPFound(self.request.route_path("my_account_configure_2fa", _query={"show-recovery-codes": 1}))
|
|
||||||
|
|
||||||
@LoginRequired()
|
@LoginRequired()
|
||||||
@NotAnonymous()
|
@NotAnonymous()
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ from rhodecode.lib.auth import (
|
||||||
NotAnonymous,
|
NotAnonymous,
|
||||||
CSRFRequired,
|
CSRFRequired,
|
||||||
)
|
)
|
||||||
|
from rhodecode.lib.auth_otp import SudoOTPRequired
|
||||||
from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
|
from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
|
||||||
from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason, unicode_to_reference
|
from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason, unicode_to_reference
|
||||||
from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError
|
from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError
|
||||||
|
|
@ -1678,6 +1679,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
||||||
@NotAnonymous()
|
@NotAnonymous()
|
||||||
@HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
|
@HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
|
||||||
@CSRFRequired()
|
@CSRFRequired()
|
||||||
|
@SudoOTPRequired
|
||||||
def pull_request_merge(self):
|
def pull_request_merge(self):
|
||||||
"""
|
"""
|
||||||
Merge will perform a server-side merge of the specified
|
Merge will perform a server-side merge of the specified
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from rhodecode.apps._base import RepoAppView
|
||||||
from rhodecode.lib import helpers as h
|
from rhodecode.lib import helpers as h
|
||||||
from rhodecode.lib import audit_logger
|
from rhodecode.lib import audit_logger
|
||||||
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, HasRepoPermissionAny
|
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, HasRepoPermissionAny
|
||||||
|
from rhodecode.lib.auth_otp import SudoOTPRequired
|
||||||
from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError
|
from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError
|
||||||
from rhodecode.lib.type_utils import str2bool
|
from rhodecode.lib.type_utils import str2bool
|
||||||
from rhodecode.lib.utils2 import safe_int
|
from rhodecode.lib.utils2 import safe_int
|
||||||
|
|
@ -142,6 +143,7 @@ class RepoSettingsAdvancedView(RepoAppView):
|
||||||
@LoginRequired()
|
@LoginRequired()
|
||||||
@HasRepoPermissionAnyDecorator("repository.admin")
|
@HasRepoPermissionAnyDecorator("repository.admin")
|
||||||
@CSRFRequired()
|
@CSRFRequired()
|
||||||
|
@SudoOTPRequired
|
||||||
def edit_advanced_delete(self):
|
def edit_advanced_delete(self):
|
||||||
"""
|
"""
|
||||||
Deletes the repository, or shows warnings if deletion is not possible
|
Deletes the repository, or shows warnings if deletion is not possible
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import click
|
||||||
|
|
||||||
from rhodecode.lib.pyramid_utils import bootstrap
|
from rhodecode.lib.pyramid_utils import bootstrap
|
||||||
from rhodecode.lib.statsd_client import StatsdClient
|
from rhodecode.lib.statsd_client import StatsdClient
|
||||||
from rhodecode.config.patches import suppress_pkg_resources_warnings
|
|
||||||
from .backends import SshWrapper
|
from .backends import SshWrapper
|
||||||
from .utils import setup_custom_logging
|
from .utils import setup_custom_logging
|
||||||
|
|
||||||
|
|
@ -53,9 +53,6 @@ log = logging.getLogger(__name__)
|
||||||
@click.option("--shell", "-s", is_flag=True, help="Allow Shell")
|
@click.option("--shell", "-s", is_flag=True, help="Allow Shell")
|
||||||
@click.option("--debug", is_flag=True, help="Enabled detailed output logging")
|
@click.option("--debug", is_flag=True, help="Enabled detailed output logging")
|
||||||
def main(ini_path, mode, user, user_id, key_id, shell, debug):
|
def main(ini_path, mode, user, user_id, key_id, shell, debug):
|
||||||
# Apply pkg_resources deprecation warning suppression early
|
|
||||||
suppress_pkg_resources_warnings()
|
|
||||||
|
|
||||||
setup_custom_logging(ini_path, debug)
|
setup_custom_logging(ini_path, debug)
|
||||||
|
|
||||||
command = os.environ.get("SSH_ORIGINAL_COMMAND", "")
|
command = os.environ.get("SSH_ORIGINAL_COMMAND", "")
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ import logging
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
|
from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
|
||||||
from rhodecode.config.patches import suppress_pkg_resources_warnings
|
|
||||||
from rhodecode.lib.request import Request
|
from rhodecode.lib.request import Request
|
||||||
from rhodecode.lib.utils2 import AttributeDict
|
from rhodecode.lib.utils2 import AttributeDict
|
||||||
from rhodecode.lib.statsd_client import StatsdClient
|
from rhodecode.lib.statsd_client import StatsdClient
|
||||||
|
|
@ -65,9 +65,6 @@ log = logging.getLogger(__name__)
|
||||||
@click.option("--shell", "-s", is_flag=True, help="Allow Shell")
|
@click.option("--shell", "-s", is_flag=True, help="Allow Shell")
|
||||||
@click.option("--debug", is_flag=True, help="Enabled detailed output logging")
|
@click.option("--debug", is_flag=True, help="Enabled detailed output logging")
|
||||||
def main(ini_path, mode, user, user_id, key_id, shell, debug):
|
def main(ini_path, mode, user, user_id, key_id, shell, debug):
|
||||||
# Apply pkg_resources deprecation warning suppression early
|
|
||||||
suppress_pkg_resources_warnings()
|
|
||||||
|
|
||||||
time_start = time.time()
|
time_start = time.time()
|
||||||
setup_custom_logging(ini_path, debug)
|
setup_custom_logging(ini_path, debug)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ def make_pyramid_app(global_config, **settings):
|
||||||
# Apply compatibility patches
|
# Apply compatibility patches
|
||||||
patches.inspect_getargspec()
|
patches.inspect_getargspec()
|
||||||
patches.repoze_sendmail_lf_fix()
|
patches.repoze_sendmail_lf_fix()
|
||||||
patches.suppress_pkg_resources_warnings()
|
|
||||||
|
|
||||||
# first init, so load_pyramid_enviroment, can access some critical data, like __file__
|
# first init, so load_pyramid_enviroment, can access some critical data, like __file__
|
||||||
propagate_rhodecode_config(global_config, {}, {}, full=False)
|
propagate_rhodecode_config(global_config, {}, {}, full=False)
|
||||||
|
|
|
||||||
|
|
@ -171,15 +171,3 @@ def repoze_sendmail_lf_fix():
|
||||||
encoding.encode_message = lambda message, *args, **kwargs: message.as_bytes(policy=SMTP)
|
encoding.encode_message = lambda message, *args, **kwargs: message.as_bytes(policy=SMTP)
|
||||||
|
|
||||||
|
|
||||||
def suppress_pkg_resources_warnings():
|
|
||||||
"""
|
|
||||||
Suppress pkg_resources deprecation warnings from any module.
|
|
||||||
|
|
||||||
pkg_resources is deprecated as of setuptools 81+ and will be removed in 2025-11.
|
|
||||||
This warning is generated by various modules including pyramid's asset resolution code
|
|
||||||
which we cannot easily modify. The warning is informational only and does not affect functionality.
|
|
||||||
"""
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", message="pkg_resources is deprecated as an API.*", category=UserWarning)
|
|
||||||
warnings.filterwarnings("ignore", message="pkg_resources is deprecated as an API.*", category=DeprecationWarning)
|
|
||||||
|
|
|
||||||
58
rhodecode/lib/auth_otp.py
Normal file
58
rhodecode/lib/auth_otp.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
|
from rhodecode.lib import helpers as h
|
||||||
|
from rhodecode.model.meta import Session
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SUDO_GRACE_PERIOD = 600 # 10 minutes
|
||||||
|
|
||||||
|
|
||||||
|
class SudoOTPRequired(object):
|
||||||
|
def __init__(self, func):
|
||||||
|
functools.update_wrapper(self, func)
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def __get__(self, obj, cls):
|
||||||
|
return functools.partial(self.__call__, obj)
|
||||||
|
|
||||||
|
def __call__(self, method_self, *args, **kwargs):
|
||||||
|
session = method_self.request.session
|
||||||
|
verified_at = session.get("sudo_otp_verified_at", 0)
|
||||||
|
|
||||||
|
if (time.time() - verified_at) < SUDO_GRACE_PERIOD:
|
||||||
|
return self.func(method_self, *args, **kwargs)
|
||||||
|
|
||||||
|
# Need sudo verification — generate OTP and redirect
|
||||||
|
user = method_self._rhodecode_db_user
|
||||||
|
code = user.generate_otp("sudo", digits=8)
|
||||||
|
Session().commit()
|
||||||
|
|
||||||
|
import rhodecode
|
||||||
|
smtp_server = rhodecode.CONFIG.get('smtp_server')
|
||||||
|
log_code = code if not smtp_server else 'X' * len(code)
|
||||||
|
log.info('[OTP] code=%s email=%s purpose=sudo', log_code, user.email)
|
||||||
|
|
||||||
|
# Send OTP email
|
||||||
|
from rhodecode.lib.celerylib import tasks, run_task
|
||||||
|
from rhodecode.model.notification import EmailNotificationModel
|
||||||
|
|
||||||
|
email_kwargs = {
|
||||||
|
'otp_code': code,
|
||||||
|
'purpose': 'sudo',
|
||||||
|
}
|
||||||
|
(subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
|
||||||
|
EmailNotificationModel.TYPE_OTP_CODE, **email_kwargs
|
||||||
|
)
|
||||||
|
run_task(tasks.send_email, [user.email], subject, email_body_plaintext, email_body)
|
||||||
|
|
||||||
|
# Store where to go after verification
|
||||||
|
session["sudo_next_url"] = method_self.request.url
|
||||||
|
if hasattr(session, 'save'):
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
raise HTTPFound(method_self.request.route_path("sudo_otp_verify"))
|
||||||
|
|
@ -155,10 +155,13 @@ def detect_vcs_request(environ, backends):
|
||||||
"_file_store++",
|
"_file_store++",
|
||||||
# login
|
# login
|
||||||
f"{ADMIN_PREFIX}/login",
|
f"{ADMIN_PREFIX}/login",
|
||||||
|
f"{ADMIN_PREFIX}/login/verify",
|
||||||
f"{ADMIN_PREFIX}/logout",
|
f"{ADMIN_PREFIX}/logout",
|
||||||
# 2fa
|
# otp
|
||||||
f"{ADMIN_PREFIX}/check_2fa",
|
f"{ADMIN_PREFIX}/otp/resend",
|
||||||
f"{ADMIN_PREFIX}/setup_2fa",
|
f"{ADMIN_PREFIX}/sudo/verify",
|
||||||
|
# register redirects to login
|
||||||
|
f"{ADMIN_PREFIX}/register",
|
||||||
# _admin/api is safe too
|
# _admin/api is safe too
|
||||||
f"{ADMIN_PREFIX}/api",
|
f"{ADMIN_PREFIX}/api",
|
||||||
# _admin/gist is safe too
|
# _admin/gist is safe too
|
||||||
|
|
|
||||||
|
|
@ -1017,6 +1017,53 @@ class User(Base, BaseModel):
|
||||||
Session().commit()
|
Session().commit()
|
||||||
return new_recovery_codes
|
return new_recovery_codes
|
||||||
|
|
||||||
|
def generate_otp(self, purpose, digits=6):
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
code = ''.join(str(secrets.randbelow(10)) for _ in range(digits))
|
||||||
|
code_hash = hashlib.sha256(code.encode('utf-8')).hexdigest()
|
||||||
|
self.update_userdata(
|
||||||
|
otp_code=code_hash,
|
||||||
|
otp_created_at=time.time(),
|
||||||
|
otp_attempts=0,
|
||||||
|
otp_purpose=purpose,
|
||||||
|
)
|
||||||
|
return code
|
||||||
|
|
||||||
|
def verify_otp(self, code, purpose, max_age=300, max_attempts=5):
|
||||||
|
import hashlib
|
||||||
|
stored_hash = self.user_data.get('otp_code')
|
||||||
|
created_at = self.user_data.get('otp_created_at', 0)
|
||||||
|
attempts = self.user_data.get('otp_attempts', 0)
|
||||||
|
stored_purpose = self.user_data.get('otp_purpose')
|
||||||
|
|
||||||
|
if not stored_hash or stored_purpose != purpose:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if attempts >= max_attempts:
|
||||||
|
self.clear_otp()
|
||||||
|
return False
|
||||||
|
|
||||||
|
if (time.time() - created_at) > max_age:
|
||||||
|
self.clear_otp()
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.update_userdata(otp_attempts=attempts + 1)
|
||||||
|
|
||||||
|
code_hash = hashlib.sha256(code.encode('utf-8')).hexdigest()
|
||||||
|
if code_hash == stored_hash:
|
||||||
|
self.clear_otp()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear_otp(self):
|
||||||
|
self.update_userdata(
|
||||||
|
otp_code=None,
|
||||||
|
otp_created_at=None,
|
||||||
|
otp_attempts=None,
|
||||||
|
otp_purpose=None,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def extra_valid_auth_tokens(cls, user, role=None):
|
def extra_valid_auth_tokens(cls, user, role=None):
|
||||||
tokens = (
|
tokens = (
|
||||||
|
|
|
||||||
|
|
@ -86,54 +86,22 @@ def LoginForm(localizer):
|
||||||
class _LoginForm(formencode.Schema):
|
class _LoginForm(formencode.Schema):
|
||||||
allow_extra_fields = True
|
allow_extra_fields = True
|
||||||
filter_extra_fields = True
|
filter_extra_fields = True
|
||||||
username = v.UnicodeString(
|
email = All(v.Email(not_empty=True), v.UnicodeString(strip=True, min=1, not_empty=True))
|
||||||
strip=True,
|
|
||||||
min=1,
|
|
||||||
not_empty=True,
|
|
||||||
messages={
|
|
||||||
"empty": _("Please enter a login"),
|
|
||||||
"tooShort": _("Enter a value %(min)i characters long or more"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
password = v.UnicodeString(
|
|
||||||
strip=False,
|
|
||||||
min=3,
|
|
||||||
max=72,
|
|
||||||
not_empty=True,
|
|
||||||
messages={"empty": _("Please enter a password"), "tooShort": _("Enter %(min)i characters or more")},
|
|
||||||
)
|
|
||||||
|
|
||||||
remember = v.StringBoolean(if_missing=False)
|
remember = v.StringBoolean(if_missing=False)
|
||||||
|
|
||||||
chained_validators = [v.ValidAuth(localizer)]
|
|
||||||
|
|
||||||
return _LoginForm
|
return _LoginForm
|
||||||
|
|
||||||
|
|
||||||
def TOTPForm(localizer, user, allow_recovery_code_use=False):
|
def OTPVerifyForm(localizer, digits=6):
|
||||||
_ = localizer
|
_ = localizer
|
||||||
|
|
||||||
class _TOTPForm(formencode.Schema):
|
class _OTPVerifyForm(formencode.Schema):
|
||||||
allow_extra_fields = True
|
allow_extra_fields = True
|
||||||
filter_extra_fields = False
|
filter_extra_fields = True
|
||||||
totp = v.Regex(r"^(?:\d{6}|[A-Z0-9]{32})$")
|
otp = v.Regex(r'^\d{%d}$' % digits, not_empty=True,
|
||||||
secret_totp = v.String()
|
messages={'invalid': _('Please enter a valid %d-digit code') % digits})
|
||||||
|
|
||||||
def to_python(self, value, state=None):
|
return _OTPVerifyForm
|
||||||
validation_checks = [user.is_totp_valid]
|
|
||||||
if allow_recovery_code_use:
|
|
||||||
validation_checks.append(user.is_2fa_recovery_code_valid)
|
|
||||||
form_data = super().to_python(value, state)
|
|
||||||
received_code = form_data["totp"]
|
|
||||||
secret = form_data.get("secret_totp")
|
|
||||||
|
|
||||||
if not any(map(lambda func: func(received_code, secret), validation_checks)):
|
|
||||||
error_msg = _("Code is invalid. Try again!")
|
|
||||||
raise formencode.Invalid(error_msg, v, state, error_dict={"totp": error_msg})
|
|
||||||
return form_data
|
|
||||||
|
|
||||||
return _TOTPForm
|
|
||||||
|
|
||||||
|
|
||||||
def WhitelistedVcsClientsForm(localizer):
|
def WhitelistedVcsClientsForm(localizer):
|
||||||
|
|
@ -246,32 +214,11 @@ def RegisterForm(localizer, edit=False, old_data=None):
|
||||||
class _RegisterForm(formencode.Schema):
|
class _RegisterForm(formencode.Schema):
|
||||||
allow_extra_fields = True
|
allow_extra_fields = True
|
||||||
filter_extra_fields = True
|
filter_extra_fields = True
|
||||||
username = All(v.ValidUsername(localizer, edit, old_data), v.UnicodeString(strip=True, min=1, not_empty=True))
|
|
||||||
password = All(v.ValidPassword(localizer), v.UnicodeString(strip=False, min=6, max=72, not_empty=True))
|
|
||||||
password_confirmation = All(
|
|
||||||
v.ValidPassword(localizer), v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
|
|
||||||
)
|
|
||||||
active = v.StringBoolean(if_missing=False)
|
|
||||||
firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
|
|
||||||
lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
|
|
||||||
email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
|
email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
|
||||||
|
|
||||||
chained_validators = [v.ValidPasswordsMatch(localizer)]
|
|
||||||
|
|
||||||
return _RegisterForm
|
return _RegisterForm
|
||||||
|
|
||||||
|
|
||||||
def PasswordResetForm(localizer):
|
|
||||||
_ = localizer
|
|
||||||
|
|
||||||
class _PasswordResetForm(formencode.Schema):
|
|
||||||
allow_extra_fields = True
|
|
||||||
filter_extra_fields = True
|
|
||||||
email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
|
|
||||||
|
|
||||||
return _PasswordResetForm
|
|
||||||
|
|
||||||
|
|
||||||
def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
|
def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
|
||||||
_ = localizer
|
_ = localizer
|
||||||
old_data = old_data or {}
|
old_data = old_data or {}
|
||||||
|
|
|
||||||
|
|
@ -378,13 +378,12 @@ class EmailNotificationModel(BaseModel):
|
||||||
TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
|
TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
|
||||||
TYPE_MAIN = Notification.TYPE_MESSAGE
|
TYPE_MAIN = Notification.TYPE_MESSAGE
|
||||||
|
|
||||||
TYPE_PASSWORD_RESET = "password_reset"
|
|
||||||
TYPE_PASSWORD_RESET_CONFIRMATION = "password_reset_confirmation"
|
|
||||||
TYPE_EMAIL_TEST = "email_test"
|
TYPE_EMAIL_TEST = "email_test"
|
||||||
TYPE_EMAIL_EXCEPTION = "exception"
|
TYPE_EMAIL_EXCEPTION = "exception"
|
||||||
TYPE_UPDATE_AVAILABLE = "update_available"
|
TYPE_UPDATE_AVAILABLE = "update_available"
|
||||||
TYPE_TEST = "test"
|
TYPE_TEST = "test"
|
||||||
TYPE_SECURITY_AUDIT = "security_audit"
|
TYPE_SECURITY_AUDIT = "security_audit"
|
||||||
|
TYPE_OTP_CODE = "otp_code"
|
||||||
|
|
||||||
email_types = {
|
email_types = {
|
||||||
TYPE_MAIN: "rhodecode:templates/email_templates/main.mako",
|
TYPE_MAIN: "rhodecode:templates/email_templates/main.mako",
|
||||||
|
|
@ -393,13 +392,12 @@ class EmailNotificationModel(BaseModel):
|
||||||
TYPE_UPDATE_AVAILABLE: "rhodecode:templates/email_templates/update_available.mako",
|
TYPE_UPDATE_AVAILABLE: "rhodecode:templates/email_templates/update_available.mako",
|
||||||
TYPE_EMAIL_TEST: "rhodecode:templates/email_templates/email_test.mako",
|
TYPE_EMAIL_TEST: "rhodecode:templates/email_templates/email_test.mako",
|
||||||
TYPE_REGISTRATION: "rhodecode:templates/email_templates/user_registration.mako",
|
TYPE_REGISTRATION: "rhodecode:templates/email_templates/user_registration.mako",
|
||||||
TYPE_PASSWORD_RESET: "rhodecode:templates/email_templates/password_reset.mako",
|
|
||||||
TYPE_PASSWORD_RESET_CONFIRMATION: "rhodecode:templates/email_templates/password_reset_confirmation.mako",
|
|
||||||
TYPE_COMMIT_COMMENT: "rhodecode:templates/email_templates/commit_comment.mako",
|
TYPE_COMMIT_COMMENT: "rhodecode:templates/email_templates/commit_comment.mako",
|
||||||
TYPE_PULL_REQUEST: "rhodecode:templates/email_templates/pull_request_review.mako",
|
TYPE_PULL_REQUEST: "rhodecode:templates/email_templates/pull_request_review.mako",
|
||||||
TYPE_PULL_REQUEST_COMMENT: "rhodecode:templates/email_templates/pull_request_comment.mako",
|
TYPE_PULL_REQUEST_COMMENT: "rhodecode:templates/email_templates/pull_request_comment.mako",
|
||||||
TYPE_PULL_REQUEST_UPDATE: "rhodecode:templates/email_templates/pull_request_update.mako",
|
TYPE_PULL_REQUEST_UPDATE: "rhodecode:templates/email_templates/pull_request_update.mako",
|
||||||
TYPE_SECURITY_AUDIT: "rhodecode:templates/email_templates/security_audit.mako",
|
TYPE_SECURITY_AUDIT: "rhodecode:templates/email_templates/security_audit.mako",
|
||||||
|
TYPE_OTP_CODE: "rhodecode:templates/email_templates/otp_code.mako",
|
||||||
}
|
}
|
||||||
|
|
||||||
premailer_instance = premailer.Premailer(
|
premailer_instance = premailer.Premailer(
|
||||||
|
|
|
||||||
|
|
@ -423,37 +423,6 @@ def ValidPasswordsMatch(localizer, passwd="new_password", passwd_confirmation="p
|
||||||
return _validator
|
return _validator
|
||||||
|
|
||||||
|
|
||||||
def ValidAuth(localizer):
|
|
||||||
_ = localizer
|
|
||||||
|
|
||||||
class _validator(formencode.validators.FancyValidator):
|
|
||||||
messages = {
|
|
||||||
"invalid_password": _("invalid password"),
|
|
||||||
"invalid_username": _("invalid user name"),
|
|
||||||
"disabled_account": _("Your account is disabled"),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _validate_python(self, value, state):
|
|
||||||
from rhodecode.authentication.base import authenticate, HTTP_TYPE
|
|
||||||
|
|
||||||
password = value["password"]
|
|
||||||
username = value["username"]
|
|
||||||
|
|
||||||
if not authenticate(username, password, "", HTTP_TYPE, skip_missing=True):
|
|
||||||
user = User.get_by_username_or_primary_email(username)
|
|
||||||
if user and not user.active:
|
|
||||||
log.warning("user %s is disabled", username)
|
|
||||||
msg = M(self, "disabled_account", state)
|
|
||||||
raise formencode.Invalid(msg, value, state, error_dict={"username": msg})
|
|
||||||
else:
|
|
||||||
log.warning("user `%s` failed to authenticate", username)
|
|
||||||
msg = M(self, "invalid_username", state)
|
|
||||||
msg2 = M(self, "invalid_password", state)
|
|
||||||
raise formencode.Invalid(msg, value, state, error_dict={"username": msg, "password": msg2})
|
|
||||||
|
|
||||||
return _validator
|
|
||||||
|
|
||||||
|
|
||||||
def ValidRepoName(localizer, edit=False, old_data=None):
|
def ValidRepoName(localizer, edit=False, old_data=None):
|
||||||
old_data = old_data or {}
|
old_data = old_data or {}
|
||||||
_ = localizer
|
_ = localizer
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -13,10 +13,10 @@ function setRCMouseBindings(repoName, repoLandingRev) {
|
||||||
return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
|
return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
|
||||||
};
|
};
|
||||||
|
|
||||||
// general help "?"
|
// general help "?" — disabled, modal requires jQuery+Bootstrap
|
||||||
Mousetrap.bind(['?'], function(e) {
|
// Mousetrap.bind(['?'], function(e) {
|
||||||
$('#help_kb').modal({});
|
// $('#help_kb').modal({});
|
||||||
});
|
// });
|
||||||
|
|
||||||
// / open the quick filter
|
// / open the quick filter
|
||||||
Mousetrap.bind(['/'], function(e) {
|
Mousetrap.bind(['/'], function(e) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<%inherit file="/base/base.mako"/>
|
<%inherit file="/base/base.mako"/>
|
||||||
|
|
||||||
<%def name="title()">
|
<%def name="title()">
|
||||||
${_('Settings administration')}
|
${_('Admin')}
|
||||||
%if c.rhodecode_name:
|
%if c.rhodecode_name:
|
||||||
· ${h.branding(c.rhodecode_name)}
|
· ${h.branding(c.rhodecode_name)}
|
||||||
%endif
|
%endif
|
||||||
|
|
@ -17,51 +17,43 @@
|
||||||
${self.admin_menu()}
|
${self.admin_menu()}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="side_bar_nav()">
|
|
||||||
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="main_content()">
|
|
||||||
<h2>${_('Administration area')}</h2>
|
|
||||||
|
|
||||||
<table class="rctable">
|
|
||||||
<tr>
|
|
||||||
<td>${_('Repositories under administration')}</td>
|
|
||||||
<td class="delegated-admin-repos">${len(c.auth_user.repositories_admin)}</td>
|
|
||||||
<td>
|
|
||||||
% if c.can_create_repo:
|
|
||||||
<a href="${h.route_path('repo_new')}" class="">${_('Add Repository')}</a>
|
|
||||||
% endif
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>${_('Repository groups under administration')}</td>
|
|
||||||
<td class="delegated-admin-repo-groups">${len(c.auth_user.repository_groups_admin)}</td>
|
|
||||||
<td>
|
|
||||||
% if c.can_create_repo_group:
|
|
||||||
<a href="${h.route_path('repo_group_new')}" class="">${_('Add Repository Group')}</a>
|
|
||||||
% endif
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>${_('User groups under administration')}</td>
|
|
||||||
<td class="delegated-admin-user-groups">${len(c.auth_user.user_groups_admin)}</td>
|
|
||||||
<td>
|
|
||||||
% if c.can_create_user_group:
|
|
||||||
<a href="${h.route_path('user_groups_new')}" class="">${_('Add User Group')}</a>
|
|
||||||
% endif
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="main()">
|
<%def name="main()">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
<div class="admin-dashboard">
|
||||||
|
<h2>${_('Admin Dashboard')}</h2>
|
||||||
|
|
||||||
##main
|
<div class="dashboard-cards">
|
||||||
<div class="main-content-auto-width">
|
<a href="${h.route_path('repos')}" class="dashboard-card">
|
||||||
${self.main_content()}
|
<span class="dashboard-card-count delegated-admin-repos">${len(c.auth_user.repositories_admin)}</span>
|
||||||
|
<span class="dashboard-card-label">${_('Repositories')}</span>
|
||||||
|
</a>
|
||||||
|
<a href="${h.route_path('repo_groups')}" class="dashboard-card">
|
||||||
|
<span class="dashboard-card-count delegated-admin-repo-groups">${len(c.auth_user.repository_groups_admin)}</span>
|
||||||
|
<span class="dashboard-card-label">${_('Repo Groups')}</span>
|
||||||
|
</a>
|
||||||
|
<a href="${h.route_path('user_groups')}" class="dashboard-card">
|
||||||
|
<span class="dashboard-card-count delegated-admin-user-groups">${len(c.auth_user.user_groups_admin)}</span>
|
||||||
|
<span class="dashboard-card-label">${_('User Groups')}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-actions">
|
||||||
|
<h3>${_('Quick Actions')}</h3>
|
||||||
|
<div class="dashboard-action-grid">
|
||||||
|
% if c.can_create_repo:
|
||||||
|
<a href="${h.route_path('repo_new')}" class="btn btn-default">${_('New Repository')}</a>
|
||||||
|
% endif
|
||||||
|
% if c.can_create_repo_group:
|
||||||
|
<a href="${h.route_path('repo_group_new')}" class="btn btn-default">${_('New Repo Group')}</a>
|
||||||
|
% endif
|
||||||
|
% if c.can_create_user_group:
|
||||||
|
<a href="${h.route_path('user_groups_new')}" class="btn btn-default">${_('New User Group')}</a>
|
||||||
|
% endif
|
||||||
|
% if c.is_super_admin:
|
||||||
|
<a href="${h.route_path('admin_settings')}" class="btn btn-default">${_('Settings')}</a>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</%def>
|
||||||
</%def>
|
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="${h.is_active(['profile', 'profile_edit'], c.active)}"><a href="${h.route_path('my_account_profile')}">${_('Profile')}</a></li>
|
<li class="${h.is_active(['profile', 'profile_edit'], c.active)}"><a href="${h.route_path('my_account_profile')}">${_('Profile')}</a></li>
|
||||||
<li class="${h.is_active('emails', c.active)}"><a href="${h.route_path('my_account_emails')}">${_('Emails')}</a></li>
|
<li class="${h.is_active('emails', c.active)}"><a href="${h.route_path('my_account_emails')}">${_('Emails')}</a></li>
|
||||||
<li class="${h.is_active('password', c.active)}"><a href="${h.route_path('my_account_password')}">${_('Password')}</a></li>
|
|
||||||
<li class="${h.is_active('2fa', c.active)}"><a href="${h.route_path('my_account_configure_2fa')}">${_('2FA')}</a></li>
|
|
||||||
<li class="${h.is_active('bookmarks', c.active)}"><a href="${h.route_path('my_account_bookmarks')}">${_('Bookmarks')}</a></li>
|
<li class="${h.is_active('bookmarks', c.active)}"><a href="${h.route_path('my_account_bookmarks')}">${_('Bookmarks')}</a></li>
|
||||||
<li class="${h.is_active('auth_tokens', c.active)}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
|
<li class="${h.is_active('auth_tokens', c.active)}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
|
||||||
<li class="${h.is_active(['ssh_keys', 'ssh_keys_generate'], c.active)}"><a href="${h.route_path('my_account_ssh_keys')}">${_('SSH Keys')}</a></li>
|
<li class="${h.is_active(['ssh_keys', 'ssh_keys_generate'], c.active)}"><a href="${h.route_path('my_account_ssh_keys')}">${_('SSH Keys')}</a></li>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="repo-header-row">
|
<div class="repo-header-row">
|
||||||
<span class="help-block-repo-create">
|
<span class="help-block-repo-create">
|
||||||
${_('Parent Repository Group / Repository Group Name')}.
|
${_('Parent / Group Name')} — use <code>/</code> to create nested groups, e.g. <code>org/team/project</code>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,10 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
|
||||||
|
|
||||||
<div class="repo-header-row">
|
<div class="repo-header-row">
|
||||||
<span class="help-block-repo-create">
|
<span class="help-block-repo-create">
|
||||||
${_('Repository Group / Repository Name')}.
|
${_('Group / Repository Name')}
|
||||||
</span>
|
</span>
|
||||||
<div class="import-repo">
|
<div class="import-repo">
|
||||||
<a id="remote_clone_toggle" href="#">${_('Import an existing repository.')}</a>
|
<a id="remote_clone_toggle" href="#">${_('Import existing')}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
<!-- END MENU BAR NAV -->
|
<!-- END MENU BAR NAV -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${self.menu_bar_subnav()}
|
|
||||||
<!-- END HEADER -->
|
<!-- END HEADER -->
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
|
|
@ -34,8 +33,19 @@
|
||||||
|
|
||||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
<rhodecode-toast id="notifications"></rhodecode-toast>
|
||||||
|
|
||||||
<div class="main">
|
<div class="main page-with-sidebar">
|
||||||
${next.main()}
|
<%
|
||||||
|
not_anon = c.rhodecode_user.username != h.DEFAULT_USER
|
||||||
|
has_sidebar = hasattr(next, 'page_sidebar') or not_anon
|
||||||
|
%>
|
||||||
|
% if has_sidebar:
|
||||||
|
<nav class="page-sidebar" id="page-sidebar">
|
||||||
|
${self.page_sidebar()}
|
||||||
|
</nav>
|
||||||
|
% endif
|
||||||
|
<div class="page-content">
|
||||||
|
${next.main()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -44,38 +54,61 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- FOOTER -->
|
<!-- FOOTER -->
|
||||||
<div id="footer">
|
<footer id="footer" class="site-footer">
|
||||||
<div id="footer-inner" class="title wrapper">
|
<div class="footer-grid">
|
||||||
<div>
|
<div class="footer-brand">
|
||||||
<% sid = 'block' if request.GET.get('showrcid') else 'none' %>
|
<a href="${h.route_path('home')}" class="footer-logo">RhodeCode</a>
|
||||||
|
<p class="footer-tagline">
|
||||||
|
Self-hosted source code management for Mercurial, Git, and Subversion.
|
||||||
|
</p>
|
||||||
|
<p class="footer-version">
|
||||||
|
% if c.visual.show_version:
|
||||||
|
v${c.rhodecode_version}
|
||||||
|
% endif
|
||||||
|
${c.rhodecode_edition}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="footer-link-right">
|
<div class="footer-group">
|
||||||
<a class="grey-link-action" href="${h.route_path('home', _query={'showrcid': 1})}">
|
<span class="footer-group-title">${_('Navigate')}</span>
|
||||||
RhodeCode
|
<nav class="footer-links">
|
||||||
% if c.visual.show_version:
|
<a href="${h.route_path('home')}">${_('Repositories')}</a>
|
||||||
${c.rhodecode_version}
|
<a href="${h.route_path('journal')}">${_('Activity')}</a>
|
||||||
% endif
|
<a href="${h.route_path('gists_show')}">${_('Gists')}</a>
|
||||||
${c.rhodecode_edition}
|
<a href="${h.route_path('my_account_pullrequests')}">${_('Pull Requests')}</a>
|
||||||
</a> |
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
% if c.visual.rhodecode_support_url:
|
<div class="footer-group">
|
||||||
<a class="grey-link-action" href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a> |
|
<span class="footer-group-title">${_('Account')}</span>
|
||||||
<a class="grey-link-action" href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
|
<nav class="footer-links">
|
||||||
% endif
|
<a href="${h.route_path('my_account_profile')}">${_('My Account')}</a>
|
||||||
|
<a href="${h.route_path('my_account_repos')}">${_('My Repos')}</a>
|
||||||
|
<a href="${h.route_path('my_account_notifications')}">${_('Notifications')}</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
</p>
|
<div class="footer-group">
|
||||||
|
<span class="footer-group-title">${_('Resources')}</span>
|
||||||
<p class="server-instance" style="display:${sid}">
|
<nav class="footer-links">
|
||||||
## display hidden instance ID if specially defined
|
<a href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
|
||||||
© 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
|
<a href="${h.route_path('apiv2')}">${_('API')}</a>
|
||||||
% if c.rhodecode_instanceid:
|
<a href="/_admin/styleguide">${_('Styleguide')}</a>
|
||||||
${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
|
% if c.visual.rhodecode_support_url:
|
||||||
% endif
|
<a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
|
||||||
</p>
|
% endif
|
||||||
</div>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-bottom">
|
||||||
|
RhodeCode © 2010–${h.datetime.today().year}
|
||||||
|
— Self-hosted source code management.
|
||||||
|
% if c.rhodecode_instanceid:
|
||||||
|
<span class="footer-instance">Instance: ${c.rhodecode_instanceid}</span>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
<!-- END FOOTER -->
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
### MAKO DEFS ###
|
### MAKO DEFS ###
|
||||||
|
|
@ -83,6 +116,33 @@
|
||||||
<%def name="menu_bar_subnav()">
|
<%def name="menu_bar_subnav()">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="page_sidebar()">
|
||||||
|
<%
|
||||||
|
not_anon = c.rhodecode_user.username != h.DEFAULT_USER
|
||||||
|
can_create_repos = c.is_super_admin or c.can_create_repo
|
||||||
|
can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
|
||||||
|
%>
|
||||||
|
|
||||||
|
% if not_anon:
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<h4 class="sidebar-heading">${_('Create')}</h4>
|
||||||
|
<ul class="sidebar-nav">
|
||||||
|
% if can_create_repos:
|
||||||
|
<li><a href="${h.route_path('repo_new')}"><i class="icon-plus"></i> ${_('New Repository')}</a></li>
|
||||||
|
% endif
|
||||||
|
% if can_create_repo_groups:
|
||||||
|
<li><a href="${h.route_path('repo_group_new')}"><i class="icon-plus"></i> ${_('New Group')}</a></li>
|
||||||
|
% endif
|
||||||
|
<li><a href="${h.route_path('gists_new')}"><i class="icon-plus"></i> ${_('New Gist')}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Admin subnav rendered into sidebar (populated by admin page templates)
|
||||||
|
${self.menu_bar_subnav()}
|
||||||
|
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="breadcrumbs(class_='breadcrumbs')">
|
<%def name="breadcrumbs(class_='breadcrumbs')">
|
||||||
<div class="${class_}">
|
<div class="${class_}">
|
||||||
${self.breadcrumbs_links()}
|
${self.breadcrumbs_links()}
|
||||||
|
|
@ -90,63 +150,60 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="admin_menu(active=None)">
|
<%def name="admin_menu(active=None)">
|
||||||
|
% if c.is_super_admin:
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<h4 class="sidebar-heading">${_('Manage')}</h4>
|
||||||
|
<ul class="sidebar-nav">
|
||||||
|
<li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
|
||||||
|
<li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}"><i class="icon-repo-group"></i> ${_('Repo Groups')}</a></li>
|
||||||
|
<li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}"><i class="icon-user"></i> ${_('Users')}</a></li>
|
||||||
|
<li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}"><i class="icon-group"></i> ${_('User Groups')}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="context-bar">
|
<div class="sidebar-section">
|
||||||
<div class="wrapper">
|
<h4 class="sidebar-heading">${_('System')}</h4>
|
||||||
<div class="title">
|
<ul class="sidebar-nav">
|
||||||
<div class="title-content">
|
<li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}"><i class="icon-lock"></i> ${_('Permissions')}</a></li>
|
||||||
<div class="title-main">
|
<li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}"><i class="icon-key"></i> ${_('Authentication')}</a></li>
|
||||||
% if c.is_super_admin:
|
<li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}"><i class="icon-cog"></i> ${_('Settings')}</a></li>
|
||||||
${_('Super-admin Panel')}
|
<li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}"><i class="icon-file-text"></i> ${_('Audit Logs')}</a></li>
|
||||||
% else:
|
</ul>
|
||||||
${_('Delegated Admin Panel')}
|
</div>
|
||||||
% endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul id="context-pages" class="navigation horizontal-list">
|
<div class="sidebar-section">
|
||||||
|
<h4 class="sidebar-heading">${_('Advanced')}</h4>
|
||||||
|
<ul class="sidebar-nav">
|
||||||
|
<li class="${h.is_active('security', active)}"><a href="${h.route_path('admin_security')}"><i class="icon-shield"></i> ${_('Security')}</a></li>
|
||||||
|
<li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}"><i class="icon-plug"></i> ${_('Integrations')}</a></li>
|
||||||
|
<li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}"><i class="icon-wrench"></i> ${_('Defaults')}</a></li>
|
||||||
|
<li class="${h.is_active('artifacts', active)}"><a href="${h.route_path('admin_artifacts')}"><i class="icon-archive"></i> ${_('Artifacts')}</a></li>
|
||||||
|
<li class="${h.is_active('automation', active)}"><a href="${h.route_path('admin_automation')}"><i class="icon-repeat"></i> ${_('Automation')}</a></li>
|
||||||
|
<li class="${h.is_active('scheduler', active)}"><a href="${h.route_path('admin_scheduler')}"><i class="icon-calendar"></i> ${_('Scheduler')}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
## super-admin case (Top Menu)
|
% elif c.is_delegated_admin:
|
||||||
% if c.is_super_admin:
|
<%
|
||||||
<li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
|
repositories = c.auth_user.repositories_admin or c.can_create_repo
|
||||||
<li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
|
repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
|
||||||
<li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
|
user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
|
||||||
<li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
|
%>
|
||||||
<li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
|
<div class="sidebar-section">
|
||||||
<li class="${h.is_active('security', active)}"><a href="${h.route_path('admin_security')}">${_('Security')}</a></li>
|
<h4 class="sidebar-heading">${_('Manage')}</h4>
|
||||||
<li class="${h.is_active('artifacts', active)}"><a href="${h.route_path('admin_artifacts')}">${_('Artifacts')}</a></li>
|
<ul class="sidebar-nav">
|
||||||
<li class="${h.is_active('automation', active)}"><a href="${h.route_path('admin_automation')}">${_('Automation')}</a></li>
|
% if repositories:
|
||||||
<li class="${h.is_active('scheduler', active)}"><a href="${h.route_path('admin_scheduler')}">${_('Scheduler')}</a></li>
|
<li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
|
||||||
<li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
|
% endif
|
||||||
<li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
|
% if repository_groups:
|
||||||
<li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
|
<li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}"><i class="icon-repo-group"></i> ${_('Repo Groups')}</a></li>
|
||||||
<li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
|
% endif
|
||||||
<li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
|
% if user_groups:
|
||||||
|
<li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}"><i class="icon-group"></i> ${_('User Groups')}</a></li>
|
||||||
## delegated admin
|
|
||||||
% elif c.is_delegated_admin:
|
|
||||||
<%
|
|
||||||
repositories=c.auth_user.repositories_admin or c.can_create_repo
|
|
||||||
repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
|
|
||||||
user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
|
|
||||||
%>
|
|
||||||
|
|
||||||
%if repositories:
|
|
||||||
<li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
|
|
||||||
%endif
|
|
||||||
%if repository_groups:
|
|
||||||
<li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
|
|
||||||
%endif
|
|
||||||
%if user_groups:
|
|
||||||
<li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
|
|
||||||
%endif
|
|
||||||
% endif
|
% endif
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
% endif
|
||||||
<div class="clear"></div>
|
|
||||||
</div>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="dt_info_panel(elements)">
|
<%def name="dt_info_panel(elements)">
|
||||||
|
|
@ -469,7 +526,7 @@
|
||||||
</li>
|
</li>
|
||||||
% if c.is_super_admin or group_admin:
|
% if c.is_super_admin or group_admin:
|
||||||
<li class="${h.is_active('settings', active)}">
|
<li class="${h.is_active('settings', active)}">
|
||||||
<a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
|
<a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Settings')}</div></a>
|
||||||
</li>
|
</li>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
|
@ -610,81 +667,53 @@
|
||||||
<div class="user-menu submenu">
|
<div class="user-menu submenu">
|
||||||
<div id="quick_login">
|
<div id="quick_login">
|
||||||
%if c.rhodecode_user.username != h.DEFAULT_USER:
|
%if c.rhodecode_user.username != h.DEFAULT_USER:
|
||||||
<div class="">
|
<div class="user-menu-header">
|
||||||
<div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
|
<div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
|
||||||
<div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
|
<div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
|
||||||
<div class="email">${c.rhodecode_user.email}</div>
|
<div class="email">${c.rhodecode_user.email}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
|
||||||
<ol class="links">
|
|
||||||
<li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
|
|
||||||
% if c.rhodecode_user.personal_repo_group:
|
|
||||||
<li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
|
|
||||||
% endif
|
|
||||||
<li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
|
|
||||||
|
|
||||||
% if c.debug_style:
|
<div class="user-menu-links">
|
||||||
<li>
|
<a href="${h.route_path('my_account_profile')}">${_('My Account')}</a>
|
||||||
<a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
|
% if c.rhodecode_user.personal_repo_group:
|
||||||
<div class="menulabel">${_('[Style]')}</div>
|
<a href="${h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name)}">${_('My Personal Group')}</a>
|
||||||
</a>
|
% endif
|
||||||
</li>
|
<a href="${h.route_path('my_account_pullrequests')}">${_('Pull Requests')}</a>
|
||||||
% endif
|
<a href="${h.route_path('my_account_repos')}">${_('My Repos')}</a>
|
||||||
|
<a href="${h.route_path('notifications_show_all')}">${_('Notifications')}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
## bookmark-items
|
% if c.bookmark_items:
|
||||||
<li class="bookmark-items">
|
<div class="user-menu-bookmarks">
|
||||||
${_('Bookmarks')}
|
<div class="user-menu-bookmarks-header">
|
||||||
<div class="pull-right">
|
<span>${_('Bookmarks')}</span>
|
||||||
<a href="${h.route_path('my_account_bookmarks')}">
|
<a href="${h.route_path('my_account_bookmarks')}"><i class="icon-cog"></i></a>
|
||||||
|
</div>
|
||||||
<i class="icon-cog"></i>
|
% for item in c.bookmark_items:
|
||||||
</a>
|
% if item.repo_id:
|
||||||
</div>
|
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
|
||||||
</li>
|
|
||||||
% if not c.bookmark_items:
|
|
||||||
<li>
|
|
||||||
<a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
|
|
||||||
</li>
|
|
||||||
% endif
|
|
||||||
% for item in c.bookmark_items:
|
|
||||||
<li>
|
|
||||||
% if item.repo_id:
|
|
||||||
<div>
|
|
||||||
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
|
|
||||||
<code>${item.position}</code>
|
<code>${item.position}</code>
|
||||||
% if item.repo_type == 'hg':
|
|
||||||
<i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
|
|
||||||
% elif item.repo_type == 'git':
|
|
||||||
<i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
|
|
||||||
% elif item.repo_type == 'svn':
|
|
||||||
<i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
|
|
||||||
% endif
|
|
||||||
${(item.title or h.shorter(item.repo_name, 30))}
|
${(item.title or h.shorter(item.repo_name, 30))}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
% elif item.group_id:
|
||||||
% elif item.group_id:
|
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
|
||||||
<div>
|
|
||||||
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
|
|
||||||
<code>${item.position}</code>
|
<code>${item.position}</code>
|
||||||
<i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
|
|
||||||
${(item.title or h.shorter(item.group_name, 30))}
|
${(item.title or h.shorter(item.group_name, 30))}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
% else:
|
||||||
% else:
|
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
|
||||||
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
|
|
||||||
<code>${item.position}</code>
|
<code>${item.position}</code>
|
||||||
${item.title}
|
${item.title}
|
||||||
</a>
|
</a>
|
||||||
% endif
|
% endif
|
||||||
</li>
|
% endfor
|
||||||
% endfor
|
</div>
|
||||||
|
% endif
|
||||||
|
|
||||||
<li class="logout">
|
<div class="user-menu-logout">
|
||||||
${h.secure_form(h.route_path('logout'), request=request)}
|
${h.secure_form(h.route_path('logout'), request=request)}
|
||||||
${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
|
${h.submit('log_out', _(u'Sign Out'),class_="btn-signout")}
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
%endif
|
%endif
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -805,35 +834,35 @@
|
||||||
|
|
||||||
## ROOT MENU
|
## ROOT MENU
|
||||||
<li class="${h.is_active('home', active)}">
|
<li class="${h.is_active('home', active)}">
|
||||||
<a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
|
<a class="menulink" href="${h.route_path('home')}">
|
||||||
<div class="menulabel">${_('Home')}</div>
|
<div class="menulabel">${_('Repos')}</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
%if c.rhodecode_user.username != h.DEFAULT_USER:
|
%if c.rhodecode_user.username != h.DEFAULT_USER:
|
||||||
<li class="${h.is_active('journal', active)}">
|
<li class="${h.is_active('journal', active)}">
|
||||||
<a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
|
<a class="menulink" href="${h.route_path('journal')}">
|
||||||
<div class="menulabel">${_('Journal')}</div>
|
<div class="menulabel">${_('Activity')}</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
%else:
|
%else:
|
||||||
<li class="${h.is_active('journal', active)}">
|
<li class="${h.is_active('journal', active)}">
|
||||||
<a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
|
<a class="menulink" href="${h.route_path('journal_public')}">
|
||||||
<div class="menulabel">${_('Public journal')}</div>
|
<div class="menulabel">${_('Activity')}</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
<li class="${h.is_active('gists', active)}">
|
<li class="${h.is_active('gists', active)}">
|
||||||
<a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
|
<a class="menulink" href="${h.route_path('gists_show')}">
|
||||||
<div class="menulabel">${_('Gists')}</div>
|
<div class="menulabel">${_('Gists')}</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
% if c.is_super_admin or c.is_delegated_admin:
|
% if c.is_super_admin or c.is_delegated_admin:
|
||||||
<li class="${h.is_active('admin', active)}">
|
<li class="${h.is_active('admin', active)}">
|
||||||
<a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
|
<a class="menulink" href="${h.route_path('admin_home')}">
|
||||||
<div class="menulabel">${_('Admin')} </div>
|
<div class="menulabel">${_('Admin')}</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
% endif
|
% endif
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,6 @@ c.template_context['attachment_store'] = {
|
||||||
## CSS definitions
|
## CSS definitions
|
||||||
<%def name="css()">
|
<%def name="css()">
|
||||||
<link rel="stylesheet" type="text/css" href="${h.asset('css/styleguide.css', ver=c.rhodecode_version_hash)}" media="screen"/>
|
<link rel="stylesheet" type="text/css" href="${h.asset('css/styleguide.css', ver=c.rhodecode_version_hash)}" media="screen"/>
|
||||||
<link rel="stylesheet" type="text/css" href="${h.asset('css/filepond.css', ver=c.rhodecode_version_hash)}" media="screen"/>
|
|
||||||
<link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
|
|
||||||
## EXTRA FOR CSS
|
## EXTRA FOR CSS
|
||||||
${self.css_extra()}
|
${self.css_extra()}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
@ -138,13 +136,9 @@ c.template_context['attachment_store'] = {
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<%include file="/base/plugins_base.mako"/>
|
<%include file="/base/plugins_base.mako"/>
|
||||||
<!--[if lt IE 9]>
|
<script src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
|
||||||
<script language="javascript" type="text/javascript" src="${h.asset('js/src/excanvas.min.js')}"></script>
|
|
||||||
<![endif]-->
|
|
||||||
<script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
|
|
||||||
<script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
|
<script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
|
||||||
## avoide escaping the %N
|
<script src="${h.asset('js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
|
||||||
<script language="javascript" type="text/javascript" src="${h.asset('js/scripts.min.js', ver=c.rhodecode_version_hash)}"></script>
|
|
||||||
<script src="${h.asset('js/ajax.js', ver=c.rhodecode_version_hash)}"></script>
|
<script src="${h.asset('js/ajax.js', ver=c.rhodecode_version_hash)}"></script>
|
||||||
|
|
||||||
## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
|
## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
<%inherit file="base/root.mako"/>
|
|
||||||
|
|
||||||
<%def name="title()">
|
|
||||||
${_('Setup 2FA')}
|
|
||||||
%if c.rhodecode_name:
|
|
||||||
· ${h.branding(c.rhodecode_name)}
|
|
||||||
%endif
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<style>body{background-color:#eeeeee;}</style>
|
|
||||||
|
|
||||||
<div class="loginbox" style="width: 600px">
|
|
||||||
|
|
||||||
<div class="header-account">
|
|
||||||
<div id="header-inner" class="title">
|
|
||||||
<div id="logo">
|
|
||||||
% if c.rhodecode_name:
|
|
||||||
<div class="branding">
|
|
||||||
<a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
|
|
||||||
</div>
|
|
||||||
% endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="loginwrapper">
|
|
||||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
|
||||||
|
|
||||||
<div class="sign-in-title">
|
|
||||||
<h1>${_('Set up the authenticator app')} - ${_('scan the QR code')}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="inner form">
|
|
||||||
${h.secure_form(h.route_path('setup_2fa'), request=request, id='totp_form')}
|
|
||||||
<strong>${_('Use an authenticator app to scan.')}</strong><br/>
|
|
||||||
|
|
||||||
## QR CODE
|
|
||||||
<code>${_('Account')}: ${totp_name}</code><br/>
|
|
||||||
<div class="qr-code-container">
|
|
||||||
<img alt="qr-code" src="data:image/png;base64, ${qr}"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="alternativeCode" style="margin: -10px 0 5px 0">${_('Unable to scan?')} <a id="toggleLink">${_('Click here')}</a></div>
|
|
||||||
|
|
||||||
## Secret alternative code
|
|
||||||
<div id="secretDiv" style="display: none">
|
|
||||||
|
|
||||||
<div style="padding: 10px 0">
|
|
||||||
<strong style="padding: 4px 0">${_('Copy and use this code to manually set up an authenticator app')}</strong>
|
|
||||||
<code>${key}</code><i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${key}" title="${_('Copy the secret key')}" ></i><br/>
|
|
||||||
<code>${_('type')}: time-based</code>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label for="totp">${_('Verify the code from the app')}:</label>
|
|
||||||
${h.text('totp', class_='form-control', )}
|
|
||||||
<div id="formErrors">
|
|
||||||
% if 'totp' in errors:
|
|
||||||
<span class="error-message">${errors.get('totp')}</span>
|
|
||||||
<br />
|
|
||||||
% endif
|
|
||||||
% if 'secret_totp' in errors:
|
|
||||||
<span class="error-message">SECRET:${errors.get('secret_totp')}</span>
|
|
||||||
<br />
|
|
||||||
% endif
|
|
||||||
</div>
|
|
||||||
${h.hidden('secret_totp', key)}
|
|
||||||
${h.submit('verify_2fa',_('Verify'), class_="btn sign-in")}
|
|
||||||
|
|
||||||
${h.end_form()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
|
|
||||||
$( "#toggleLink" ).on("click", function() {
|
|
||||||
$( "#secretDiv" ).toggle();
|
|
||||||
$( "#alternativeCode").hide();
|
|
||||||
$('#totp').focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#totp').focus();
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
25
rhodecode/templates/email_templates/otp_code.mako
Normal file
25
rhodecode/templates/email_templates/otp_code.mako
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<%inherit file="base.mako"/>
|
||||||
|
<%namespace name="base" file="base.mako"/>
|
||||||
|
|
||||||
|
<%def name="subject()" filter="n,trim,whitespace_filter">
|
||||||
|
Your verification code
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
## plain text version of the email
|
||||||
|
<%def name="body_plaintext()" filter="n,trim">
|
||||||
|
Your verification code is: ${otp_code}
|
||||||
|
|
||||||
|
This code expires in 5 minutes. If you did not request this code, please ignore this email.
|
||||||
|
|
||||||
|
---
|
||||||
|
${self.plaintext_footer()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
## BODY GOES BELOW
|
||||||
|
<p>Your verification code is:</p>
|
||||||
|
<p style="font-size: 32px; font-family: 'Menlo', 'Consolas', monospace; letter-spacing: 0.3em; font-weight: bold; padding: 16px; background-color: #f5f5f5; border-radius: 4px; text-align: center;">
|
||||||
|
${otp_code}
|
||||||
|
</p>
|
||||||
|
<p style="color: #6b7280; font-size: 13px;">
|
||||||
|
This code expires in 5 minutes. If you did not request this code, please ignore this email.
|
||||||
|
</p>
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<%inherit file="base.mako"/>
|
|
||||||
<%namespace name="base" file="base.mako"/>
|
|
||||||
|
|
||||||
<%def name="subject()" filter="n,trim,whitespace_filter">
|
|
||||||
RhodeCode Password reset
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
## plain text version of the email. Empty by default
|
|
||||||
<%def name="body_plaintext()" filter="n,trim">
|
|
||||||
Hello ${user.username},
|
|
||||||
|
|
||||||
On ${h.format_date(date)} there was a request to reset your password using the email address `${email}`
|
|
||||||
|
|
||||||
*If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}*
|
|
||||||
|
|
||||||
You can continue, and generate new password by clicking following URL:
|
|
||||||
${password_reset_url}
|
|
||||||
|
|
||||||
This link will be active for 10 minutes.
|
|
||||||
|
|
||||||
---
|
|
||||||
${self.plaintext_footer()}
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
## BODY GOES BELOW
|
|
||||||
<p>
|
|
||||||
Hello ${user.username},
|
|
||||||
</p><p>
|
|
||||||
On ${h.format_date(date)} there was a request to reset your password using the email address `${email}`
|
|
||||||
<br/><br/>
|
|
||||||
<strong>If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}.</strong>
|
|
||||||
</p><p>
|
|
||||||
You can continue, and generate new password by clicking following URL:<br/><br/>
|
|
||||||
<a href="${password_reset_url}" style="${base.link_css()}">${password_reset_url}</a>
|
|
||||||
<br/><br/>This link will be active for 10 minutes.
|
|
||||||
</p>
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<%inherit file="base.mako"/>
|
|
||||||
<%namespace name="base" file="base.mako"/>
|
|
||||||
|
|
||||||
<%def name="subject()" filter="n,trim,whitespace_filter">
|
|
||||||
Your new RhodeCode password
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
## plain text version of the email. Empty by default
|
|
||||||
<%def name="body_plaintext()" filter="n,trim">
|
|
||||||
Hello ${user.username},
|
|
||||||
|
|
||||||
Below is your new access password for RhodeCode requested via password reset link.
|
|
||||||
|
|
||||||
*If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}.*
|
|
||||||
|
|
||||||
new password: ${new_password}
|
|
||||||
|
|
||||||
---
|
|
||||||
${self.plaintext_footer()}
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
## BODY GOES BELOW
|
|
||||||
<p>
|
|
||||||
Hello ${user.username},
|
|
||||||
</p><p>
|
|
||||||
Below is your new access password for RhodeCode requested via password reset link.
|
|
||||||
<br/><br/>
|
|
||||||
<strong>If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}.</strong>
|
|
||||||
</p>
|
|
||||||
<p>new password: <code>${new_password}</code>
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
· ${h.branding(c.rhodecode_name)}
|
· ${h.branding(c.rhodecode_name)}
|
||||||
%endif
|
%endif
|
||||||
</%def>
|
</%def>
|
||||||
<style>body{background-color:#eeeeee;}</style>
|
|
||||||
|
|
||||||
<div class="loginbox">
|
<div class="loginbox">
|
||||||
<div class="header-account">
|
<div class="header-account">
|
||||||
|
|
@ -24,80 +23,35 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="loginwrapper">
|
<rhodecode-toast id="notifications"></rhodecode-toast>
|
||||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
|
||||||
|
|
||||||
|
<div class="loginwrapper">
|
||||||
<div class="auth-image-wrapper">
|
<div class="auth-image-wrapper">
|
||||||
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
|
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="login">
|
<div id="login">
|
||||||
<%block name="above_login_button" />
|
<%block name="above_login_button" />
|
||||||
<!-- login -->
|
|
||||||
<div class="sign-in-title">
|
<div class="sign-in-title">
|
||||||
<h1>${_('Sign In using credentials')}</h1>
|
<h1>${_('Sign In')}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="inner form">
|
<div class="inner form">
|
||||||
${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
|
${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
|
||||||
|
|
||||||
<label for="username">${_('Username or email address')}:</label>
|
<label for="email">${_('Email address')}:</label>
|
||||||
${h.text('username', class_='focus', value=defaults.get('username'))}
|
${h.text('email', class_='focus', value=defaults.get('email'))}
|
||||||
%if 'username' in errors:
|
%if 'email' in errors:
|
||||||
<span class="error-message">${errors.get('username')}</span>
|
<span class="error-message">${errors.get('email')}</span>
|
||||||
<br />
|
|
||||||
%endif
|
|
||||||
|
|
||||||
<label for="password">${_('Password')}:
|
|
||||||
%if h.HasPermissionAny('hg.password_reset.enabled')():
|
|
||||||
<div class="pull-right">${h.link_to(_('Forgot your password?'), h.route_path('reset_password'), class_='pwd_reset', tabindex="-1")}</div>
|
|
||||||
%endif
|
|
||||||
|
|
||||||
</label>
|
|
||||||
${h.password('password', class_='focus')}
|
|
||||||
%if 'password' in errors:
|
|
||||||
<span class="error-message">${errors.get('password')}</span>
|
|
||||||
<br />
|
<br />
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
${h.checkbox('remember', value=True, checked=defaults.get('remember'))}
|
${h.checkbox('remember', value=True, checked=defaults.get('remember'))}
|
||||||
<% timeout = request.registry.settings.get('session.timeout', request.registry.settings.get('beaker.session.timeout', '0')) %>
|
<label class="checkbox" for="remember">${_('Remember me')}</label>
|
||||||
% if timeout == '0':
|
|
||||||
<% remember_label = _('Remember my indefinitely') %>
|
|
||||||
% else:
|
|
||||||
<% remember_label = _('Remember me for {}').format(h.age_from_seconds(timeout)) %>
|
|
||||||
% endif
|
|
||||||
<label class="checkbox" for="remember">${remember_label}</label>
|
|
||||||
|
|
||||||
<p class="links">
|
${h.submit('sign_in', _('Send code'), class_="btn sign-in", title=_('Send verification code'))}
|
||||||
%if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
|
|
||||||
${h.link_to(_("Create a new account."), request.route_path('register'), class_='new_account')}
|
|
||||||
%endif
|
|
||||||
</p>
|
|
||||||
|
|
||||||
%if not h.HasPermissionAny('hg.password_reset.enabled')():
|
|
||||||
## password reset hidden or disabled.
|
|
||||||
<p class="help-block">
|
|
||||||
${_('Password reset is disabled.')} <br/>
|
|
||||||
${_('Please contact ')}
|
|
||||||
% if c.visual.rhodecode_support_url:
|
|
||||||
<a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
|
|
||||||
${_('or')}
|
|
||||||
% endif
|
|
||||||
${_('an administrator if you need help.')}
|
|
||||||
</p>
|
|
||||||
%endif
|
|
||||||
|
|
||||||
${h.submit('sign_in', _('Sign In'), class_="btn sign-in", title=_('Sign in to {}').format(c.rhodecode_edition))}
|
|
||||||
|
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
<script type="text/javascript">
|
|
||||||
$(document).ready(function(){
|
|
||||||
$('#username').focus();
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- end login -->
|
|
||||||
|
|
||||||
<%block name="below_login_button" />
|
<%block name="below_login_button" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
97
rhodecode/templates/otp_verify.mako
Normal file
97
rhodecode/templates/otp_verify.mako
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<%inherit file="base/root.mako"/>
|
||||||
|
|
||||||
|
<%def name="title()">
|
||||||
|
${_('Verify Code')}
|
||||||
|
%if c.rhodecode_name:
|
||||||
|
· ${h.branding(c.rhodecode_name)}
|
||||||
|
%endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<div class="loginbox">
|
||||||
|
<div class="header-account">
|
||||||
|
<div id="header-inner" class="title">
|
||||||
|
<div id="logo">
|
||||||
|
<div class="logo-wrapper">
|
||||||
|
<a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
|
||||||
|
</div>
|
||||||
|
% if c.rhodecode_name:
|
||||||
|
<div class="branding">
|
||||||
|
<a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<rhodecode-toast id="notifications"></rhodecode-toast>
|
||||||
|
|
||||||
|
<div class="loginwrapper">
|
||||||
|
<div class="auth-image-wrapper">
|
||||||
|
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="login">
|
||||||
|
<div class="sign-in-title">
|
||||||
|
<h1>${_('Enter verification code')}</h1>
|
||||||
|
<h4>${_('We sent a {}-digit code to {}').format(otp_digits, otp_email)}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="inner form">
|
||||||
|
${h.form(request.route_path(verify_route), needs_csrf_token=False)}
|
||||||
|
|
||||||
|
<label for="otp">${_('Verification code')}:</label>
|
||||||
|
${h.text('otp', class_='focus', inputmode='numeric', autocomplete='one-time-code', maxlength=str(otp_digits), value=defaults.get('otp', ''))}
|
||||||
|
%if 'otp' in errors:
|
||||||
|
<span class="error-message">${errors.get('otp')}</span>
|
||||||
|
<br />
|
||||||
|
%endif
|
||||||
|
|
||||||
|
<p class="help-block">
|
||||||
|
${_('Check your email for the verification code. It expires in 5 minutes.')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
${h.submit('verify', _('Verify'), class_="btn sign-in")}
|
||||||
|
|
||||||
|
${h.end_form()}
|
||||||
|
|
||||||
|
<p class="links" style="margin-top: 15px;">
|
||||||
|
<button type="button" class="btn btn-link" id="resend-otp">${_('Resend code')}</button>
|
||||||
|
<span id="resend-status" style="display:none; color: var(--text-muted, #6b7280); font-size: 13px;"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var btn = document.getElementById('resend-otp');
|
||||||
|
var status = document.getElementById('resend-status');
|
||||||
|
if (btn) {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
btn.disabled = true;
|
||||||
|
status.style.display = 'inline';
|
||||||
|
status.textContent = 'Sending...';
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '${request.route_path("resend_otp")}');
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
xhr.onload = function() {
|
||||||
|
var resp = JSON.parse(xhr.responseText);
|
||||||
|
status.textContent = resp.message || 'Done';
|
||||||
|
setTimeout(function() {
|
||||||
|
btn.disabled = false;
|
||||||
|
status.style.display = 'none';
|
||||||
|
}, 60000);
|
||||||
|
};
|
||||||
|
xhr.onerror = function() {
|
||||||
|
status.textContent = 'Error sending code.';
|
||||||
|
btn.disabled = false;
|
||||||
|
};
|
||||||
|
xhr.send('{}');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var otpInput = document.querySelector('input[name="otp"]');
|
||||||
|
if (otpInput) otpInput.focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
<%inherit file="base/root.mako"/>
|
|
||||||
|
|
||||||
<%def name="title()">
|
|
||||||
${_('Reset Password')}
|
|
||||||
%if c.rhodecode_name:
|
|
||||||
· ${h.branding(c.rhodecode_name)}
|
|
||||||
%endif
|
|
||||||
</%def>
|
|
||||||
<style>body{background-color:#eeeeee;}</style>
|
|
||||||
|
|
||||||
<div class="loginbox">
|
|
||||||
<div class="header-account">
|
|
||||||
<div id="header-inner" class="title">
|
|
||||||
<div id="logo">
|
|
||||||
<div class="logo-wrapper">
|
|
||||||
<a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
|
|
||||||
</div>
|
|
||||||
% if c.rhodecode_name:
|
|
||||||
<div class="branding">
|
|
||||||
<a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
|
|
||||||
</div>
|
|
||||||
% endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="loginwrapper">
|
|
||||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
|
||||||
|
|
||||||
<div class="auth-image-wrapper">
|
|
||||||
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
%if h.HasPermissionAny('hg.password_reset.disabled')():
|
|
||||||
<div class="right-column">
|
|
||||||
<p>
|
|
||||||
${_('Password reset is disabled. Please contact ')}
|
|
||||||
% if c.visual.rhodecode_support_url:
|
|
||||||
<a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
|
|
||||||
${_('or')}
|
|
||||||
% endif
|
|
||||||
${_('an administrator if you need help.')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
%else:
|
|
||||||
<div id="register">
|
|
||||||
<!-- login -->
|
|
||||||
<div class="sign-in-title">
|
|
||||||
<h1>${_('Reset your Password')}</h1>
|
|
||||||
<h4>${h.link_to(_("Go to the login page to sign in."), request.route_path('login'))}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="inner form">
|
|
||||||
${h.form(request.route_path('reset_password'), needs_csrf_token=False)}
|
|
||||||
<label for="email">${_('Email Address')}:</label>
|
|
||||||
${h.text('email', defaults.get('email'))}
|
|
||||||
%if 'email' in errors:
|
|
||||||
<span class="error-message">${errors.get('email')}</span>
|
|
||||||
<br />
|
|
||||||
%endif
|
|
||||||
<p class="help-block">${_('Password reset link will be sent to matching email address')}</p>
|
|
||||||
|
|
||||||
%if captcha_active:
|
|
||||||
<div class="login-captcha">
|
|
||||||
<label for="email">${_('Captcha')}:</label>
|
|
||||||
${h.hidden('recaptcha_field')}
|
|
||||||
<div id="recaptcha"></div>
|
|
||||||
|
|
||||||
%if 'recaptcha_field' in errors:
|
|
||||||
<span class="error-message">${errors.get('recaptcha_field')}</span>
|
|
||||||
<br />
|
|
||||||
%endif
|
|
||||||
</div>
|
|
||||||
%endif
|
|
||||||
|
|
||||||
${h.submit('send', _('Send password reset email'), class_="btn sign-in")}
|
|
||||||
<p class="help-block pull-right">
|
|
||||||
RhodeCode ${c.rhodecode_edition}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
${h.end_form()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
%endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(document).ready(function(){
|
|
||||||
$('#email').focus();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
% if captcha_active:
|
|
||||||
<script type="text/javascript">
|
|
||||||
var onloadCallback = function() {
|
|
||||||
grecaptcha.render('recaptcha', {
|
|
||||||
'sitekey' : "${captcha_public_key}"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
|
|
||||||
% endif
|
|
||||||
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
· ${h.branding(c.rhodecode_name)}
|
· ${h.branding(c.rhodecode_name)}
|
||||||
%endif
|
%endif
|
||||||
</%def>
|
</%def>
|
||||||
<style>body{background-color:#eeeeee;}</style>
|
|
||||||
|
|
||||||
<div class="loginbox">
|
<div class="loginbox">
|
||||||
<div class="header-account">
|
<div class="header-account">
|
||||||
|
|
@ -24,77 +23,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="loginwrapper">
|
<rhodecode-toast id="notifications"></rhodecode-toast>
|
||||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
|
||||||
|
|
||||||
|
<div class="loginwrapper">
|
||||||
<div class="auth-image-wrapper">
|
<div class="auth-image-wrapper">
|
||||||
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
|
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="register">
|
<div id="register">
|
||||||
<%block name="above_register_button" />
|
<%block name="above_register_button" />
|
||||||
<!-- login -->
|
|
||||||
<div class="sign-in-title">
|
<div class="sign-in-title">
|
||||||
% if external_auth_provider:
|
<h1>${_('Create an account')}</h1>
|
||||||
<h1>${_('Create an account linked with {}').format(external_auth_provider)}</h1>
|
|
||||||
% else:
|
|
||||||
<h1>${_('Create an account')}</h1>
|
|
||||||
% endif
|
|
||||||
|
|
||||||
<h4>${h.link_to(_("Go to the login page to sign in with an existing account."), request.route_path('login'))}</h4>
|
<h4>${h.link_to(_("Go to the login page to sign in with an existing account."), request.route_path('login'))}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="inner form">
|
<div class="inner form">
|
||||||
${h.form(request.route_path('register'), needs_csrf_token=False)}
|
${h.form(request.route_path('register'), needs_csrf_token=False)}
|
||||||
|
|
||||||
<label for="username">${_('Username')}:</label>
|
<label for="email">${_('Email address')}:</label>
|
||||||
${h.text('username', defaults.get('username'))}
|
${h.text('email', defaults.get('email'), class_='focus')}
|
||||||
%if 'username' in errors:
|
|
||||||
<span class="error-message">${errors.get('username')}</span>
|
|
||||||
<br />
|
|
||||||
%endif
|
|
||||||
|
|
||||||
% if external_auth_provider:
|
|
||||||
## store internal marker about external identity
|
|
||||||
${h.hidden('external_identity', external_auth_provider)}
|
|
||||||
## hide password prompts for social auth
|
|
||||||
<div style="display: none">
|
|
||||||
% endif
|
|
||||||
|
|
||||||
<label for="password">${_('Password')}:</label>
|
|
||||||
${h.password('password', defaults.get('password'))}
|
|
||||||
%if 'password' in errors:
|
|
||||||
<span class="error-message">${errors.get('password')}</span>
|
|
||||||
<br />
|
|
||||||
%endif
|
|
||||||
|
|
||||||
<label for="password_confirmation">${_('Re-enter password')}:</label>
|
|
||||||
${h.password('password_confirmation', defaults.get('password_confirmation'))}
|
|
||||||
%if 'password_confirmation' in errors:
|
|
||||||
<span class="error-message">${errors.get('password_confirmation')}</span>
|
|
||||||
<br />
|
|
||||||
%endif
|
|
||||||
|
|
||||||
% if external_auth_provider:
|
|
||||||
## hide password prompts for social auth
|
|
||||||
</div>
|
|
||||||
% endif
|
|
||||||
|
|
||||||
<label for="firstname">${_('First Name')}:</label>
|
|
||||||
${h.text('firstname', defaults.get('firstname'))}
|
|
||||||
%if 'firstname' in errors:
|
|
||||||
<span class="error-message">${errors.get('firstname')}</span>
|
|
||||||
<br />
|
|
||||||
%endif
|
|
||||||
|
|
||||||
<label for="lastname">${_('Last Name')}:</label>
|
|
||||||
${h.text('lastname', defaults.get('lastname'))}
|
|
||||||
%if 'lastname' in errors:
|
|
||||||
<span class="error-message">${errors.get('lastname')}</span>
|
|
||||||
<br />
|
|
||||||
%endif
|
|
||||||
|
|
||||||
<label for="email">${_('Email')}:</label>
|
|
||||||
${h.text('email', defaults.get('email'))}
|
|
||||||
%if 'email' in errors:
|
%if 'email' in errors:
|
||||||
<span class="error-message">${errors.get('email')}</span>
|
<span class="error-message">${errors.get('email')}</span>
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -121,7 +67,7 @@
|
||||||
${register_message|n}
|
${register_message|n}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
${h.submit('sign_up',_('Create Account'), class_="btn sign-in", title=_('Create Account in {}').format(c.rhodecode_edition))}
|
${h.submit('sign_up', _('Send code'), class_="btn sign-in", title=_('Send verification code'))}
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
</div>
|
</div>
|
||||||
<%block name="below_register_button" />
|
<%block name="below_register_button" />
|
||||||
|
|
@ -129,13 +75,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(document).ready(function(){
|
|
||||||
$('#username').focus();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
% if captcha_active:
|
% if captcha_active:
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var onloadCallback = function() {
|
var onloadCallback = function() {
|
||||||
|
|
@ -146,4 +85,3 @@ var onloadCallback = function() {
|
||||||
</script>
|
</script>
|
||||||
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
|
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
|
|
||||||
65
rhodecode/templates/sudo_otp_verify.mako
Normal file
65
rhodecode/templates/sudo_otp_verify.mako
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<%inherit file="base/root.mako"/>
|
||||||
|
|
||||||
|
<%def name="title()">
|
||||||
|
${_('Confirm Your Identity')}
|
||||||
|
%if c.rhodecode_name:
|
||||||
|
· ${h.branding(c.rhodecode_name)}
|
||||||
|
%endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<div class="loginbox">
|
||||||
|
<div class="header-account">
|
||||||
|
<div id="header-inner" class="title">
|
||||||
|
<div id="logo">
|
||||||
|
<div class="logo-wrapper">
|
||||||
|
<a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
|
||||||
|
</div>
|
||||||
|
% if c.rhodecode_name:
|
||||||
|
<div class="branding">
|
||||||
|
<a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<rhodecode-toast id="notifications"></rhodecode-toast>
|
||||||
|
|
||||||
|
<div class="loginwrapper">
|
||||||
|
<div class="auth-image-wrapper">
|
||||||
|
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="login">
|
||||||
|
<div class="sign-in-title">
|
||||||
|
<h1>${_('Confirm your identity')}</h1>
|
||||||
|
<h4>${_('This action requires elevated verification. We sent an 8-digit code to your email.')}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="inner form">
|
||||||
|
${h.form(request.route_path('sudo_otp_verify'), needs_csrf_token=True)}
|
||||||
|
|
||||||
|
<label for="otp">${_('Verification code')}:</label>
|
||||||
|
${h.text('otp', class_='focus', inputmode='numeric', autocomplete='one-time-code', maxlength='8', value=defaults.get('otp', ''))}
|
||||||
|
%if 'otp' in errors:
|
||||||
|
<span class="error-message">${errors.get('otp')}</span>
|
||||||
|
<br />
|
||||||
|
%endif
|
||||||
|
|
||||||
|
<p class="help-block">
|
||||||
|
${_('Enter the 8-digit code sent to your email. It expires in 5 minutes.')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
${h.submit('verify', _('Verify'), class_="btn sign-in")}
|
||||||
|
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var otpInput = document.querySelector('input[name="otp"]');
|
||||||
|
if (otpInput) otpInput.focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
<%inherit file="base/root.mako"/>
|
|
||||||
|
|
||||||
<%def name="title()">
|
|
||||||
${_('Verify 2FA')}
|
|
||||||
%if c.rhodecode_name:
|
|
||||||
· ${h.branding(c.rhodecode_name)}
|
|
||||||
%endif
|
|
||||||
</%def>
|
|
||||||
<style>body{background-color:#eeeeee;}</style>
|
|
||||||
|
|
||||||
<div class="loginbox" style="width: 600px">
|
|
||||||
<div class="header-account">
|
|
||||||
<div id="header-inner" class="title">
|
|
||||||
<div id="logo">
|
|
||||||
% if c.rhodecode_name:
|
|
||||||
<div class="branding">
|
|
||||||
<a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
|
|
||||||
</div>
|
|
||||||
% endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="loginwrapper">
|
|
||||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
|
||||||
|
|
||||||
<div id="register">
|
|
||||||
<div class="sign-in-title">
|
|
||||||
<h1>${_('Verify the code from the app')}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="inner form">
|
|
||||||
${h.secure_form(h.route_path('check_2fa'), request=request, id='totp_form')}
|
|
||||||
<label for="totp">${_('Verification code')}:</label>
|
|
||||||
${h.text('totp', class_="form-control")}
|
|
||||||
%if 'totp' in errors:
|
|
||||||
<span class="error-message">${errors.get('totp')}</span>
|
|
||||||
<br />
|
|
||||||
%endif
|
|
||||||
<p class="help-block">${_('Enter the code from your two-factor authenticator app. If you\'ve lost your device, you can enter one of your recovery codes.')}</p>
|
|
||||||
|
|
||||||
${h.submit('send', _('Verify'), class_="btn sign-in")}
|
|
||||||
<p class="help-block pull-right">
|
|
||||||
RhodeCode ${c.rhodecode_edition}
|
|
||||||
</p>
|
|
||||||
${h.end_form()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue