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:
russell@unturf.com 2026-03-04 16:44:40 -05:00
parent 1dac65fc8a
commit 722c3bd369
38 changed files with 4100 additions and 1835 deletions

View file

@ -149,7 +149,7 @@ app.base_url = http://rhodecode.local
app.service_api.host = http://rhodecode.local:10020
; 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.
app_instance_uuid = rc-production
@ -254,7 +254,7 @@ auth_ret_code_detection = false
lock_ret_code = 423
; 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
allow_custom_hooks_settings = true
@ -309,7 +309,7 @@ labs_settings_active = true
; NOTE: this setting IS DEPRECATED:
; file_store.storage_path = X -> use `file_store.filesystem_v2.storage_path = X` instead
; 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.
; 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
; 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...
#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
; 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
; the repository. This path is important to be shared across filesystems and with
; 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
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.
; 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
@ -644,7 +644,7 @@ channelstream.server = channelstream:8000
; For development, comment this out to use auto-generated proxy URL
;channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
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.
; If you use proxy-prefix the prefix should be added before /_channelstream
@ -729,10 +729,10 @@ vcs.connection_timeout = 3600
vcs.methods.cache = true
; 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
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)

9
package-lock.json generated
View file

@ -79,7 +79,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@ -1994,7 +1993,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -2021,7 +2019,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@ -2249,7 +2246,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@ -3438,7 +3434,6 @@
"integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"dateformat": "~4.6.2",
"eventemitter2": "~0.4.13",
@ -5103,7 +5098,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -5829,7 +5823,6 @@
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@ -6107,7 +6100,6 @@
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@ -6157,7 +6149,6 @@
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",

View file

@ -129,9 +129,11 @@ class TemplateArgs(StrictAttributeDict):
class BaseAppView(object):
DONT_CHECKOUT_VIEWS = ["channelstream_connect", "ops_ping"]
EXTRA_VIEWS_TO_IGNORE = ["login", "register", "logout"]
SETUP_2FA_VIEW = "setup_2fa"
VERIFY_2FA_VIEW = "check_2fa"
EXTRA_VIEWS_TO_IGNORE = [
"login", "register", "logout",
"login_otp_verify",
"sudo_otp_verify", "resend_otp",
]
def __init__(self, context, request):
self.request = request
@ -147,70 +149,6 @@ class BaseAppView(object):
self._rhodecode_user = request.user # auth user
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._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):
_ = self.request.translate

View file

@ -34,6 +34,7 @@ from rhodecode.apps._base import BaseAppView, DataGridAppView
from rhodecode.lib.auth import LoginRequired, CSRFRequired, NotAnonymous, HasPermissionAny, HasRepoGroupPermissionAny
from rhodecode.lib import helpers as h, audit_logger
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.permission import PermissionModel
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)
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))
repo_group_form = RepoGroupForm(
self.request.translate, available_groups=available_groups, can_create_in_root=can_create
)()
repo_group_name = self.request.POST.get("group_name")
try:
owner = self._rhodecode_user
form_result = repo_group_form.to_python(dict(self.request.POST))
copy_permissions = form_result.get("group_copy_permissions")
repo_group = RepoGroupModel().create(
group_name=form_result["group_name_full"],
group_description=form_result["group_description"],
@ -332,9 +338,97 @@ class AdminRepoGroupsView(BaseAppView, DataGridAppView):
return Response(html)
except Exception:
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"))
PermissionModel().trigger_permission_flush()
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))

View file

@ -45,6 +45,7 @@ from rhodecode.lib.exceptions import (
)
from rhodecode.lib import ext_json
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.helpers import SqlPage
from rhodecode.lib.utils2 import safe_int, safe_str, AttributeDict
@ -360,6 +361,7 @@ class UsersView(UserAppView):
@LoginRequired()
@HasPermissionAllDecorator("hg.admin")
@CSRFRequired()
@SudoOTPRequired
def user_delete(self):
_ = self.request.translate
c = self.load_default_context()

View file

@ -35,60 +35,61 @@ def includeme(config):
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_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_view(
LoginView,
attr="register",
route_name="register",
request_method="GET",
renderer="rhodecode:templates/register.mako",
)
config.add_view(
LoginView,
attr="register_post",
attr="register",
route_name="register",
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(
LoginView,
attr="password_reset",
route_name="reset_password",
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",
attr="sudo_otp_verify",
route_name="sudo_otp_verify",
request_method="GET",
renderer="rhodecode:templates/verify_2fa.mako",
renderer="rhodecode:templates/sudo_otp_verify.mako",
)
config.add_view(
LoginView,
attr="verify_2fa",
route_name="check_2fa",
attr="sudo_otp_verify_post",
route_name="sudo_otp_verify",
request_method="POST",
renderer="rhodecode:templates/verify_2fa.mako",
renderer="rhodecode:templates/sudo_otp_verify.mako",
)

View file

@ -16,9 +16,10 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import hashlib
import re
import secrets
import time
import pyotp
import qrcode
import dataclasses
import datetime
import formencode
@ -26,17 +27,12 @@ import formencode.htmlfill
import logging
import urllib.parse
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
import rhodecode
from rhodecode.apps._base import BaseAppView
from rhodecode.authentication.base import authenticate, HTTP_TYPE
from rhodecode.authentication.plugins import auth_rhodecode
from rhodecode.events import UserRegistered, trigger
from rhodecode.lib import helpers as h
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.exceptions import UserCreationError
from rhodecode.lib.utils2 import safe_str
from rhodecode.model.db import User, UserApiKeys
from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm
from rhodecode.model.db import User
from rhodecode.model.forms import LoginForm, OTPVerifyForm
from rhodecode.model.meta import Session
from rhodecode.model.auth_token import AuthTokenModel
from rhodecode.model.settings import SettingsModel
from rhodecode.model.user import UserModel
from rhodecode.translation import _
@ -84,15 +79,8 @@ def store_user_in_session(session, user_identifier, remember=False):
safe_cs["password"] = "****"
log.info("user %s is now authenticated and stored in session, session attrs %s", user_identifier, safe_cs)
# dumps session attrs back to cookie
if hasattr(session, '_update_cookie_out'):
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
# Cookie is set automatically by Pyramid's session response callback
return None
def get_came_from(request):
@ -114,6 +102,21 @@ def get_came_from(request):
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):
def load_default_context(self):
c = self._get_local_tmpl_context()
@ -144,6 +147,28 @@ class LoginView(BaseAppView):
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):
c = self.load_default_context()
auth_user = self._rhodecode_user
@ -171,46 +196,255 @@ class LoginView(BaseAppView):
login_form = LoginForm(self.request.translate)()
try:
self.session.invalidate()
form_result = login_form.to_python(self.request.POST)
# form checks for username/password, now we're authenticated
username = form_result["username"]
if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa:
user.check_2fa_required = True
email = form_result["email"]
remember = form_result["remember"]
headers = store_user_in_session(self.session, user_identifier=username, remember=form_result["remember"])
log.debug('Redirecting to "%s" after login.', c.came_from)
# Throttle to prevent enumeration
time.sleep(1)
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.success", action_data=action_data, user=audit_user, commit=True)
user = User.get_by_email(email)
if user and user.active:
# Existing active user — generate OTP on user object
code = user.generate_otp("login")
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:
defaults = errors.value
# remove password from filling in form again
defaults.pop("password", None)
render_ctx = {
"errors": errors.error_dict,
"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)
except UserCreationError as e:
# headers auth or other auth functions that create users on
# 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._get_template_context(c)
# ---- Login Step 2: OTP Verify ----
def login_otp_verify(self):
c = self.load_default_context()
email = self.session.get("otp_email")
if not email:
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()
def logout(self):
@ -222,296 +456,51 @@ class LoginView(BaseAppView):
self.session.delete()
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):
raise HTTPFound(self.request.route_path("login"))
# ---- Sudo OTP ----
@LoginRequired()
@NotAnonymous()
def sudo_otp_verify(self):
c = self.load_default_context()
defaults = defaults or {}
errors = errors or {}
return self._get_template_context(c)
settings = SettingsModel().get_all_settings()
register_message = settings.get("rhodecode_register_message") or ""
captcha = self._get_captcha_data()
auto_active = "hg.register.auto_activate" in User.get_default_user().AuthUser().permissions["global"]
@LoginRequired()
@NotAnonymous()
@CSRFRequired()
def sudo_otp_verify_post(self):
c = self.load_default_context()
user = self._rhodecode_db_user
render_ctx = self._get_template_context(c)
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
otp_form = OTPVerifyForm(self.request.translate, digits=8)()
@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:
form_result = register_form.to_python(self.request.POST)
form_result["active"] = auto_active
external_identity = self.request.POST.get("external_identity")
form_result = otp_form.to_python(self.request.POST)
code = form_result["otp"]
if external_identity:
extern_name = external_identity
extern_type = external_identity
if not user.verify_otp(code, "sudo"):
Session().commit()
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()
redirect_ro = self.request.route_path("login")
raise HTTPFound(redirect_ro)
self.session["sudo_otp_verified_at"] = time.time()
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:
errors.value.pop("password", None)
errors.value.pop("password_confirmation", None)
return self.register(defaults=errors.value, errors=errors.error_dict)
except UserCreationError as e:
# container auth or other auth functions that create users on
# 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)
defaults = errors.value
render_ctx = {
"errors": errors.error_dict,
"defaults": defaults,
}
return self._get_template_context(c, **render_ctx)

View file

@ -58,7 +58,6 @@ from rhodecode.model.db import (
RepoGroup,
ChangesetStatus,
)
from rhodecode.model.forms import TOTPForm
from rhodecode.model.meta import Session
from rhodecode.model.pull_request import PullRequestModel
from rhodecode.model.user import UserModel
@ -257,26 +256,8 @@ class MyAccountView(BaseAppView, DataGridAppView):
@CSRFRequired()
def my_account_2fa_regenerate_recovery_codes(self):
_ = self.request.translate
c = self.load_default_context()
user_instance = c.auth_user.get_instance()
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}))
h.flash(_("2FA recovery codes are no longer used."), category="warning")
raise HTTPFound(self.request.route_path("my_account_configure_2fa"))
@LoginRequired()
@NotAnonymous()

View file

@ -42,6 +42,7 @@ from rhodecode.lib.auth import (
NotAnonymous,
CSRFRequired,
)
from rhodecode.lib.auth_otp import SudoOTPRequired
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.exceptions import CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError
@ -1678,6 +1679,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
@NotAnonymous()
@HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
@CSRFRequired()
@SudoOTPRequired
def pull_request_merge(self):
"""
Merge will perform a server-side merge of the specified

View file

@ -26,6 +26,7 @@ from rhodecode.apps._base import RepoAppView
from rhodecode.lib import helpers as h
from rhodecode.lib import audit_logger
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.type_utils import str2bool
from rhodecode.lib.utils2 import safe_int
@ -142,6 +143,7 @@ class RepoSettingsAdvancedView(RepoAppView):
@LoginRequired()
@HasRepoPermissionAnyDecorator("repository.admin")
@CSRFRequired()
@SudoOTPRequired
def edit_advanced_delete(self):
"""
Deletes the repository, or shows warnings if deletion is not possible

View file

@ -30,7 +30,7 @@ import click
from rhodecode.lib.pyramid_utils import bootstrap
from rhodecode.lib.statsd_client import StatsdClient
from rhodecode.config.patches import suppress_pkg_resources_warnings
from .backends import SshWrapper
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("--debug", is_flag=True, help="Enabled detailed output logging")
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)
command = os.environ.get("SSH_ORIGINAL_COMMAND", "")

View file

@ -37,7 +37,7 @@ import logging
import click
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.utils2 import AttributeDict
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("--debug", is_flag=True, help="Enabled detailed output logging")
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()
setup_custom_logging(ini_path, debug)

View file

@ -106,7 +106,7 @@ def make_pyramid_app(global_config, **settings):
# Apply compatibility patches
patches.inspect_getargspec()
patches.repoze_sendmail_lf_fix()
patches.suppress_pkg_resources_warnings()
# first init, so load_pyramid_enviroment, can access some critical data, like __file__
propagate_rhodecode_config(global_config, {}, {}, full=False)

View file

@ -171,15 +171,3 @@ def repoze_sendmail_lf_fix():
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
View 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"))

View file

@ -155,10 +155,13 @@ def detect_vcs_request(environ, backends):
"_file_store++",
# login
f"{ADMIN_PREFIX}/login",
f"{ADMIN_PREFIX}/login/verify",
f"{ADMIN_PREFIX}/logout",
# 2fa
f"{ADMIN_PREFIX}/check_2fa",
f"{ADMIN_PREFIX}/setup_2fa",
# otp
f"{ADMIN_PREFIX}/otp/resend",
f"{ADMIN_PREFIX}/sudo/verify",
# register redirects to login
f"{ADMIN_PREFIX}/register",
# _admin/api is safe too
f"{ADMIN_PREFIX}/api",
# _admin/gist is safe too

View file

@ -1017,6 +1017,53 @@ class User(Base, BaseModel):
Session().commit()
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
def extra_valid_auth_tokens(cls, user, role=None):
tokens = (

View file

@ -86,54 +86,22 @@ def LoginForm(localizer):
class _LoginForm(formencode.Schema):
allow_extra_fields = True
filter_extra_fields = True
username = v.UnicodeString(
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")},
)
email = All(v.Email(not_empty=True), v.UnicodeString(strip=True, min=1, not_empty=True))
remember = v.StringBoolean(if_missing=False)
chained_validators = [v.ValidAuth(localizer)]
return _LoginForm
def TOTPForm(localizer, user, allow_recovery_code_use=False):
def OTPVerifyForm(localizer, digits=6):
_ = localizer
class _TOTPForm(formencode.Schema):
class _OTPVerifyForm(formencode.Schema):
allow_extra_fields = True
filter_extra_fields = False
totp = v.Regex(r"^(?:\d{6}|[A-Z0-9]{32})$")
secret_totp = v.String()
filter_extra_fields = True
otp = v.Regex(r'^\d{%d}$' % digits, not_empty=True,
messages={'invalid': _('Please enter a valid %d-digit code') % digits})
def to_python(self, value, state=None):
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
return _OTPVerifyForm
def WhitelistedVcsClientsForm(localizer):
@ -246,32 +214,11 @@ def RegisterForm(localizer, edit=False, old_data=None):
class _RegisterForm(formencode.Schema):
allow_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))
chained_validators = [v.ValidPasswordsMatch(localizer)]
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):
_ = localizer
old_data = old_data or {}

View file

@ -378,13 +378,12 @@ class EmailNotificationModel(BaseModel):
TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
TYPE_MAIN = Notification.TYPE_MESSAGE
TYPE_PASSWORD_RESET = "password_reset"
TYPE_PASSWORD_RESET_CONFIRMATION = "password_reset_confirmation"
TYPE_EMAIL_TEST = "email_test"
TYPE_EMAIL_EXCEPTION = "exception"
TYPE_UPDATE_AVAILABLE = "update_available"
TYPE_TEST = "test"
TYPE_SECURITY_AUDIT = "security_audit"
TYPE_OTP_CODE = "otp_code"
email_types = {
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_EMAIL_TEST: "rhodecode:templates/email_templates/email_test.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_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_UPDATE: "rhodecode:templates/email_templates/pull_request_update.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(

View file

@ -423,37 +423,6 @@ def ValidPasswordsMatch(localizer, passwd="new_password", passwd_confirmation="p
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):
old_data = old_data or {}
_ = localizer

File diff suppressed because it is too large Load diff

View file

@ -13,10 +13,10 @@ function setRCMouseBindings(repoName, repoLandingRev) {
return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
};
// general help "?"
Mousetrap.bind(['?'], function(e) {
$('#help_kb').modal({});
});
// general help "?" — disabled, modal requires jQuery+Bootstrap
// Mousetrap.bind(['?'], function(e) {
// $('#help_kb').modal({});
// });
// / open the quick filter
Mousetrap.bind(['/'], function(e) {

View file

@ -1,7 +1,7 @@
<%inherit file="/base/base.mako"/>
<%def name="title()">
${_('Settings administration')}
${_('Admin')}
%if c.rhodecode_name:
&middot; ${h.branding(c.rhodecode_name)}
%endif
@ -17,51 +17,43 @@
${self.admin_menu()}
</%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()">
<div class="box">
<div class="admin-dashboard">
<h2>${_('Admin Dashboard')}</h2>
##main
<div class="main-content-auto-width">
${self.main_content()}
<div class="dashboard-cards">
<a href="${h.route_path('repos')}" class="dashboard-card">
<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>
</%def>
</%def>

View file

@ -27,8 +27,6 @@
<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('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('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>

View file

@ -51,7 +51,7 @@
</div>
<div class="repo-header-row">
<span class="help-block-repo-create">
${_('Parent Repository Group / Repository Group Name')}.
${_('Parent / Group Name')} &mdash; use <code>/</code> to create nested groups, e.g. <code>org/team/project</code>
</span>
</div>

View file

@ -27,10 +27,10 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
<div class="repo-header-row">
<span class="help-block-repo-create">
${_('Repository Group / Repository Name')}.
${_('Group / Repository Name')}
</span>
<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>

View file

@ -26,7 +26,6 @@
<!-- END MENU BAR NAV -->
</div>
</div>
${self.menu_bar_subnav()}
<!-- END HEADER -->
<!-- CONTENT -->
@ -34,8 +33,19 @@
<rhodecode-toast id="notifications"></rhodecode-toast>
<div class="main">
${next.main()}
<div class="main page-with-sidebar">
<%
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>
@ -44,38 +54,61 @@
</div>
<!-- FOOTER -->
<div id="footer">
<div id="footer-inner" class="title wrapper">
<div>
<% sid = 'block' if request.GET.get('showrcid') else 'none' %>
<footer id="footer" class="site-footer">
<div class="footer-grid">
<div class="footer-brand">
<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">
<a class="grey-link-action" href="${h.route_path('home', _query={'showrcid': 1})}">
RhodeCode
% if c.visual.show_version:
${c.rhodecode_version}
% endif
${c.rhodecode_edition}
</a> |
<div class="footer-group">
<span class="footer-group-title">${_('Navigate')}</span>
<nav class="footer-links">
<a href="${h.route_path('home')}">${_('Repositories')}</a>
<a href="${h.route_path('journal')}">${_('Activity')}</a>
<a href="${h.route_path('gists_show')}">${_('Gists')}</a>
<a href="${h.route_path('my_account_pullrequests')}">${_('Pull Requests')}</a>
</nav>
</div>
% if c.visual.rhodecode_support_url:
<a class="grey-link-action" href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a> |
<a class="grey-link-action" href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
% endif
<div class="footer-group">
<span class="footer-group-title">${_('Account')}</span>
<nav class="footer-links">
<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>
<p class="server-instance" style="display:${sid}">
## display hidden instance ID if specially defined
&copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
% if c.rhodecode_instanceid:
${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
% endif
</p>
</div>
</div>
</div>
<div class="footer-group">
<span class="footer-group-title">${_('Resources')}</span>
<nav class="footer-links">
<a href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
<a href="${h.route_path('apiv2')}">${_('API')}</a>
<a href="/_admin/styleguide">${_('Styleguide')}</a>
% if c.visual.rhodecode_support_url:
<a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
% endif
</nav>
</div>
</div>
<div class="footer-bottom">
RhodeCode &copy; 2010&ndash;${h.datetime.today().year}
&mdash; Self-hosted source code management.
% if c.rhodecode_instanceid:
<span class="footer-instance">Instance: ${c.rhodecode_instanceid}</span>
% endif
</div>
</footer>
<!-- END FOOTER -->
### MAKO DEFS ###
@ -83,6 +116,33 @@
<%def name="menu_bar_subnav()">
</%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')">
<div class="${class_}">
${self.breadcrumbs_links()}
@ -90,63 +150,60 @@
</%def>
<%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="wrapper">
<div class="title">
<div class="title-content">
<div class="title-main">
% if c.is_super_admin:
${_('Super-admin Panel')}
% else:
${_('Delegated Admin Panel')}
% endif
</div>
</div>
</div>
<div class="sidebar-section">
<h4 class="sidebar-heading">${_('System')}</h4>
<ul class="sidebar-nav">
<li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}"><i class="icon-lock"></i> ${_('Permissions')}</a></li>
<li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}"><i class="icon-key"></i> ${_('Authentication')}</a></li>
<li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}"><i class="icon-cog"></i> ${_('Settings')}</a></li>
<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>
</ul>
</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)
% 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>
<li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
<li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
<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>
<li class="${h.is_active('security', active)}"><a href="${h.route_path('admin_security')}">${_('Security')}</a></li>
<li class="${h.is_active('artifacts', active)}"><a href="${h.route_path('admin_artifacts')}">${_('Artifacts')}</a></li>
<li class="${h.is_active('automation', active)}"><a href="${h.route_path('admin_automation')}">${_('Automation')}</a></li>
<li class="${h.is_active('scheduler', active)}"><a href="${h.route_path('admin_scheduler')}">${_('Scheduler')}</a></li>
<li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
<li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
<li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
<li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
<li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</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
% 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
%>
<div class="sidebar-section">
<h4 class="sidebar-heading">${_('Manage')}</h4>
<ul class="sidebar-nav">
% if repositories:
<li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
% endif
% if repository_groups:
<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>
% endif
% 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>
% endif
</ul>
</div>
<div class="clear"></div>
</div>
</div>
% endif
</%def>
<%def name="dt_info_panel(elements)">
@ -469,7 +526,7 @@
</li>
% if c.is_super_admin or group_admin:
<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>
% endif
@ -610,81 +667,53 @@
<div class="user-menu submenu">
<div id="quick_login">
%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="full_name">${c.rhodecode_user.full_name_or_username}</div>
<div class="email">${c.rhodecode_user.email}</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:
<li>
<a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
<div class="menulabel">${_('[Style]')}</div>
</a>
</li>
% endif
<div class="user-menu-links">
<a href="${h.route_path('my_account_profile')}">${_('My Account')}</a>
% if c.rhodecode_user.personal_repo_group:
<a href="${h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name)}">${_('My Personal Group')}</a>
% endif
<a href="${h.route_path('my_account_pullrequests')}">${_('Pull Requests')}</a>
<a href="${h.route_path('my_account_repos')}">${_('My Repos')}</a>
<a href="${h.route_path('notifications_show_all')}">${_('Notifications')}</a>
</div>
## bookmark-items
<li class="bookmark-items">
${_('Bookmarks')}
<div class="pull-right">
<a href="${h.route_path('my_account_bookmarks')}">
<i class="icon-cog"></i>
</a>
</div>
</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)}">
% if c.bookmark_items:
<div class="user-menu-bookmarks">
<div class="user-menu-bookmarks-header">
<span>${_('Bookmarks')}</span>
<a href="${h.route_path('my_account_bookmarks')}"><i class="icon-cog"></i></a>
</div>
% for item in c.bookmark_items:
% if item.repo_id:
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
<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))}
</a>
</div>
% elif item.group_id:
<div>
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
</a>
% elif item.group_id:
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
<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))}
</a>
</div>
% else:
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
</a>
% else:
<a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
<code>${item.position}</code>
${item.title}
</a>
% endif
</li>
% endfor
</a>
% endif
% endfor
</div>
% endif
<li class="logout">
${h.secure_form(h.route_path('logout'), request=request)}
${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
${h.end_form()}
</li>
</ol>
<div class="user-menu-logout">
${h.secure_form(h.route_path('logout'), request=request)}
${h.submit('log_out', _(u'Sign Out'),class_="btn-signout")}
${h.end_form()}
</div>
%endif
</div>
@ -805,35 +834,35 @@
## ROOT MENU
<li class="${h.is_active('home', active)}">
<a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
<div class="menulabel">${_('Home')}</div>
<a class="menulink" href="${h.route_path('home')}">
<div class="menulabel">${_('Repos')}</div>
</a>
</li>
%if c.rhodecode_user.username != h.DEFAULT_USER:
<li class="${h.is_active('journal', active)}">
<a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
<div class="menulabel">${_('Journal')}</div>
<a class="menulink" href="${h.route_path('journal')}">
<div class="menulabel">${_('Activity')}</div>
</a>
</li>
%else:
<li class="${h.is_active('journal', active)}">
<a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
<div class="menulabel">${_('Public journal')}</div>
<a class="menulink" href="${h.route_path('journal_public')}">
<div class="menulabel">${_('Activity')}</div>
</a>
</li>
%endif
<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>
</a>
</li>
% if c.is_super_admin or c.is_delegated_admin:
<li class="${h.is_active('admin', active)}">
<a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
<div class="menulabel">${_('Admin')} </div>
<a class="menulink" href="${h.route_path('admin_home')}">
<div class="menulabel">${_('Admin')}</div>
</a>
</li>
% endif

View file

@ -63,8 +63,6 @@ c.template_context['attachment_store'] = {
## CSS definitions
<%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/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
${self.css_extra()}
</%def>
@ -138,13 +136,9 @@ c.template_context['attachment_store'] = {
</script>
<%include file="/base/plugins_base.mako"/>
<!--[if lt IE 9]>
<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 src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
<script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
## avoide escaping the %N
<script language="javascript" type="text/javascript" src="${h.asset('js/scripts.min.js', ver=c.rhodecode_version_hash)}"></script>
<script src="${h.asset('js/scripts.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

View file

@ -1,90 +0,0 @@
<%inherit file="base/root.mako"/>
<%def name="title()">
${_('Setup 2FA')}
%if c.rhodecode_name:
&middot; ${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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -6,7 +6,6 @@
&middot; ${h.branding(c.rhodecode_name)}
%endif
</%def>
<style>body{background-color:#eeeeee;}</style>
<div class="loginbox">
<div class="header-account">
@ -24,80 +23,35 @@
</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">
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
</div>
<div id="login">
<%block name="above_login_button" />
<!-- login -->
<div class="sign-in-title">
<h1>${_('Sign In using credentials')}</h1>
<h1>${_('Sign In')}</h1>
</div>
<div class="inner form">
${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
<label for="username">${_('Username or email address')}:</label>
${h.text('username', class_='focus', value=defaults.get('username'))}
%if 'username' in errors:
<span class="error-message">${errors.get('username')}</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>
<label for="email">${_('Email address')}:</label>
${h.text('email', class_='focus', value=defaults.get('email'))}
%if 'email' in errors:
<span class="error-message">${errors.get('email')}</span>
<br />
%endif
${h.checkbox('remember', value=True, checked=defaults.get('remember'))}
<% timeout = request.registry.settings.get('session.timeout', request.registry.settings.get('beaker.session.timeout', '0')) %>
% 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>
<label class="checkbox" for="remember">${_('Remember me')}</label>
<p class="links">
%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.submit('sign_in', _('Send code'), class_="btn sign-in", title=_('Send verification code'))}
${h.end_form()}
<script type="text/javascript">
$(document).ready(function(){
$('#username').focus();
})
</script>
</div>
<!-- end login -->
<%block name="below_login_button" />
</div>

View file

@ -0,0 +1,97 @@
<%inherit file="base/root.mako"/>
<%def name="title()">
${_('Verify Code')}
%if c.rhodecode_name:
&middot; ${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>

View file

@ -1,103 +0,0 @@
<%inherit file="base/root.mako"/>
<%def name="title()">
${_('Reset Password')}
%if c.rhodecode_name:
&middot; ${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

View file

@ -6,7 +6,6 @@
&middot; ${h.branding(c.rhodecode_name)}
%endif
</%def>
<style>body{background-color:#eeeeee;}</style>
<div class="loginbox">
<div class="header-account">
@ -24,77 +23,24 @@
</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">
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
</div>
<div id="register">
<%block name="above_register_button" />
<!-- login -->
<div class="sign-in-title">
% if external_auth_provider:
<h1>${_('Create an account linked with {}').format(external_auth_provider)}</h1>
% else:
<h1>${_('Create an account')}</h1>
% endif
<h1>${_('Create an account')}</h1>
<h4>${h.link_to(_("Go to the login page to sign in with an existing account."), request.route_path('login'))}</h4>
</div>
<div class="inner form">
${h.form(request.route_path('register'), needs_csrf_token=False)}
<label for="username">${_('Username')}:</label>
${h.text('username', defaults.get('username'))}
%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'))}
<label for="email">${_('Email address')}:</label>
${h.text('email', defaults.get('email'), class_='focus')}
%if 'email' in errors:
<span class="error-message">${errors.get('email')}</span>
<br />
@ -121,7 +67,7 @@
${register_message|n}
</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()}
</div>
<%block name="below_register_button" />
@ -129,13 +75,6 @@
</div>
</div>
<script type="text/javascript">
$(document).ready(function(){
$('#username').focus();
});
</script>
% if captcha_active:
<script type="text/javascript">
var onloadCallback = function() {
@ -146,4 +85,3 @@ var onloadCallback = function() {
</script>
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
% endif

View file

@ -0,0 +1,65 @@
<%inherit file="base/root.mako"/>
<%def name="title()">
${_('Confirm Your Identity')}
%if c.rhodecode_name:
&middot; ${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>

View file

@ -1,54 +0,0 @@
<%inherit file="base/root.mako"/>
<%def name="title()">
${_('Verify 2FA')}
%if c.rhodecode_name:
&middot; ${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>