Commit aace558d authored by sergio's avatar sergio

Merge branch 'release_201804-dev' into 'release_201804'

Merge Release 201804 dev

See merge request !1
parents ea79dbf1 4a2fcfab
......@@ -4,6 +4,9 @@ env
# Private settings
mxcp/private_settings.py
# Custom conf
mxcp/custom_conf.py
# Database, and content associated images
db.sqlite3
......
import re
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
class NumberValidator(object):
def validate(self, password, user=None):
if not re.findall('\d', password):
raise ValidationError(
_("The password must contain at least 1 digit, 0-9."),
code='password_no_number',
)
def get_help_text(self):
return _(
"Your password must contain at least 1 digit, 0-9."
)
class UppercaseValidator(object):
def validate(self, password, user=None):
if not re.findall('[A-Z]', password):
raise ValidationError(
_("The password must contain at least 1 uppercase letter, A-Z."),
code='password_no_upper',
)
def get_help_text(self):
return _(
"Your password must contain at least 1 uppercase letter, A-Z."
)
class LowercaseValidator(object):
def validate(self, password, user=None):
if not re.findall('[a-z]', password):
raise ValidationError(
_("The password must contain at least 1 lowercase letter, a-z."),
code='password_no_lower',
)
def get_help_text(self):
return _(
"Your password must contain at least 1 lowercase letter, a-z."
)
class SymbolValidator(object):
def validate(self, password, user=None):
if not re.findall('[()[\]{}|\\`~!@#$%^&*_\-+=;:\'",<>./?]', password):
raise ValidationError(
_("The password must contain at least 1 symbol: " +
"()[]{}|\`~!@#$%^&*_-+=;:'\",<>./?"),
code='password_no_symbol',
)
def get_help_text(self):
return _(
"Your password must contain at least 1 symbol: " +
"()[]{}|\`~!@#$%^&*_-+=;:'\",<>./?"
)
......@@ -59,8 +59,8 @@ class GenericForm(forms.Form):
class CustomLoginForm(forms.Form):
""" Custom login form """
username = forms.CharField(label='Usuario', required=True)
password = forms.CharField(label='Contraseña', widget=forms.PasswordInput(), required=True)
username = forms.CharField(label=_('Usuario'), required=True)
password = forms.CharField(label=_('Contraseña'), widget=forms.PasswordInput(), required=True)
def as_p(self):
"Return this form rendered as HTML <p>s."
......
This diff is collapsed.
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-03-06 09:11+0100\n"
"POT-Creation-Date: 2019-03-22 08:55+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -18,6 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: auth_validators.py:11
msgid "The password must contain at least 1 digit, 0-9."
msgstr ""
#: auth_validators.py:17
msgid "Your password must contain at least 1 digit, 0-9."
msgstr ""
#: auth_validators.py:25
msgid "The password must contain at least 1 uppercase letter, A-Z."
msgstr ""
#: auth_validators.py:31
msgid "Your password must contain at least 1 uppercase letter, A-Z."
msgstr ""
#: auth_validators.py:39
msgid "The password must contain at least 1 lowercase letter, a-z."
msgstr ""
#: auth_validators.py:45
msgid "Your password must contain at least 1 lowercase letter, a-z."
msgstr ""
#: auth_validators.py:53
msgid ""
"The password must contain at least 1 symbol: ()[]{}|\\`~!@#$%^&*_-+=;:'\","
"<>./?"
msgstr ""
#: auth_validators.py:60
msgid ""
"Your password must contain at least 1 symbol: ()[]{}|\\`~!@#$%^&*_-+=;:'\","
"<>./?"
msgstr ""
#: forms.py:33
msgid "Por favor confirma la contraseña que has introducido."
msgstr ""
......@@ -26,6 +62,14 @@ msgstr ""
msgid "Las contraseñas no coinciden."
msgstr ""
#: forms.py:62 forms.py:376
msgid "Usuario"
msgstr ""
#: forms.py:63 forms.py:383 forms.py:420 forms.py:467 forms.py:653
msgid "Contraseña"
msgstr ""
#: forms.py:78 forms.py:109 forms.py:460 forms.py:652
msgid "Nombre de usuario"
msgstr ""
......@@ -178,7 +222,7 @@ msgstr ""
msgid "%s no en un nombre de dominio válido"
msgstr ""
#: forms.py:304 forms.py:719 utils.py:798
#: forms.py:304 forms.py:719 utils.py:803
#, python-format
msgid "El dominio %s ya está activado"
msgstr ""
......@@ -188,7 +232,7 @@ msgstr ""
msgid "%s es el dominio actual del sistema"
msgstr ""
#: forms.py:312 forms.py:729 utils.py:802
#: forms.py:312 forms.py:729 utils.py:807
#, python-format
msgid "El dominio %s está en uso por otra aplicación"
msgstr ""
......@@ -244,10 +288,6 @@ msgid ""
"aplicación Mailman"
msgstr ""
#: forms.py:376
msgid "Usuario"
msgstr ""
#: forms.py:377
msgid ""
"Introduce tu nombre de usuario. Por ejemplo si quieres tener un correo "
......@@ -264,10 +304,6 @@ msgid ""
"com'"
msgstr ""
#: forms.py:383 forms.py:420 forms.py:467 forms.py:653
msgid "Contraseña"
msgstr ""
#: forms.py:385 forms.py:418 forms.py:461 forms.py:587
msgid "Nombre"
msgstr ""
......@@ -462,7 +498,7 @@ msgstr ""
msgid "Recibir Logs del sistema por correo electrónico"
msgstr ""
#: forms.py:715 utils.py:796
#: forms.py:715 utils.py:801
#, python-format
msgid "%s no es un nombre de dominio válido"
msgstr ""
......@@ -477,7 +513,7 @@ msgstr ""
msgid "El dominio %s está en uso por la aplicación Mailman."
msgstr ""
#: forms.py:741 utils.py:807
#: forms.py:741 utils.py:812
#, python-format
msgid "La configuración de los DNS para El dominio %s no es correcta."
msgstr ""
......@@ -496,48 +532,48 @@ msgstr ""
msgid "Tu sesión ha caducado, por favor introduce tus credenciales nuevamente"
msgstr ""
#: utils.py:409 views_dns.py:24
#: utils.py:413 views_dns.py:24
msgid "No se ha encontrado ningún registro para el dominio."
msgstr ""
#: utils.py:410 views_dns.py:25
#: utils.py:414 views_dns.py:25
msgid ""
"El dominio tiene configurado más de un registro de tipo A. Esta "
"configuración puede provocar anomalías. A menos que sepas exactamente lo que "
"estás haciendo es aconsejable que dejes un solo registro."
msgstr ""
#: utils.py:414 views_dns.py:29
#: utils.py:418 views_dns.py:29
msgid "La configuración de DNS para el servidor web es correcta"
msgstr ""
#: utils.py:415 views_dns.py:30
#: utils.py:419 views_dns.py:30
msgid ""
"La configuración de los DNS no es correcta para el servidor web. Sigue las "
"instrucciones a continuación para corregirla"
msgstr ""
#: utils.py:417 views_dns.py:32
#: utils.py:421 views_dns.py:32
msgid ""
"El servidor de correo no está activado para este dominio. En el caso "
"quisieras activarlo la siguiente tabla te muestra los valores DNS correctos."
msgstr ""
#: utils.py:608
#: utils.py:613
msgid "Bienvenido a tu nuevo buzón. Por favor no contestes a este mensaje"
msgstr ""
#: utils.py:627
#: utils.py:632
#, python-format
msgid ""
"Hola.El administrador de %(host)s ha activado un acceso VPN para tu cuenta. "
"Usuario: %(username)sContraseña: Por razones de seguridad, no se envían "
"contraseñas por correo electrónicoi. Debes solicitarla al administrador. Por "
"contraseñas por correo electrónico. Debes solicitarla al administrador. Por "
"favor, descarga el archivo adjunto y sigue las instrucciones para tu sistema "
"operativo, disponibles en: http://docs.maadix.net/vpn/"
msgstr ""
#: utils.py:636
#: utils.py:641
#, python-format
msgid ""
"<p>Hola.</p><p>El administrador de %(host)s ha activado un acceso VPN para "
......@@ -549,11 +585,11 @@ msgid ""
"maadix.net/vpn/</a></p>"
msgstr ""
#: utils.py:800
#: utils.py:805
msgid "No puedes añadir el dominio actual del cpanel"
msgstr ""
#: utils.py:1000 views_services.py:85
#: utils.py:1005 views_services.py:94
#, python-format
msgid ""
"Esta aplicación necesita ser instalada bajo un dominio o subdominio propio. "
......@@ -561,13 +597,13 @@ msgid ""
"aplicación y cuyos dns estén ya apuntando a la IP de este servidor:%s"
msgstr ""
#: utils.py:1004 views_services.py:89
#: utils.py:1009 views_services.py:98
msgid ""
"Esta aplicación necesita una cuenta de email asociada. Inserta una dirección "
"de correo electrónico válida:"
msgstr ""
#: utils.py:1006 views_services.py:91
#: utils.py:1011 views_services.py:100
msgid "Inserta un texto"
msgstr ""
......@@ -594,7 +630,7 @@ msgstr ""
msgid "Activación realizada con éxito."
msgstr ""
#: views_activate.py:161 views_users.py:69
#: views_activate.py:161 views_users.py:74
msgid "Las credenciales son incorrectas. ¿Las has escrito correctamente?"
msgstr ""
......@@ -683,23 +719,23 @@ msgstr ""
msgid "Cuenta de correo modificada con éxito"
msgstr ""
#: views_services.py:162 views_services.py:293
#: views_services.py:180 views_services.py:311
msgid ""
"Hubo algún problema modificando el estado de tus aplicaciones. Contacta con "
"los administrador-s si el problema persiste"
msgstr ""
#: views_services.py:185
#: views_services.py:203
#, python-format
msgid "Ya existe el usuario %s. No se puede instalar la aplicación"
msgstr ""
#: views_services.py:190
#: views_services.py:208
#, python-format
msgid "Ya existe el usuario %s. Eliminalo para poder proceder"
msgstr ""
#: views_services.py:207
#: views_services.py:225
msgid "Corrige los errores marcados en rojo"
msgstr ""
......@@ -709,6 +745,22 @@ msgid ""
"persiste contacta con los administrador-s"
msgstr ""
#: views_system.py:329
msgid ""
"Se eliminarán las imágenes de docker que no están referenciadas ni usadas "
"por ningún contenedor.Si has instalado alguna imagen de docker de forma "
"manual, sin utilizar el panel de control, asegúrate de no tender ninguna que "
"no esté actualmente en uso y que quieras conservar."
msgstr ""
#: views_system.py:356 views_system.py:374
msgid "Operación guardada con éxito"
msgstr ""
#: views_system.py:358 views_system.py:376
msgid "Se ha producido un error"
msgstr ""
#: views_users.py:64
msgid ""
"Hay problemas de conectividad con la base de datos.Intenta entrar dentro de "
......@@ -716,84 +768,84 @@ msgid ""
"nosotras"
msgstr ""
#: views_users.py:107
#: views_users.py:112
msgid "Hasta la próxima!"
msgstr ""
#: views_users.py:137
#: views_users.py:142
msgid ""
"Te hemos enviado instrucciones para cambiar tu contraseña al correo "
"electrónico que has introducido"
msgstr ""
#: views_users.py:188
#: views_users.py:193
msgid "La URL no es válida o ha caducado"
msgstr ""
#: views_users.py:205
#: views_users.py:210
msgid ""
"Procesando operación. Todos los datos para recuperar la contraseña han sido "
"insertados correctamente. Recibirás un correo de confirmación en cuanto la "
"contraseña haya sido restablecida.Este proceso puede tardar hasta 10 minutos."
msgstr ""
#: views_users.py:275
#: views_users.py:280
msgid ""
"Has cambiado tu perfil con éxito. Utiliza la nueva contraseña para entrar."
msgstr ""
#: views_users.py:278
#: views_users.py:283
msgid "Has cambiado tu perfil con éxito."
msgstr ""
#: views_users.py:428
#: views_users.py:433
msgid "Usuari- añadid- con éxito"
msgstr ""
#: views_users.py:564
#: views_users.py:569
msgid "Usuario modificado con éxito"
msgstr ""
#: views_users.py:569
#: views_users.py:574
msgid "Instrucciones enviadas con éxito"
msgstr ""
#: views_users.py:571
#: views_users.py:576
msgid ""
"Se ha producio un fallo en el envío de las intrucciones para la conexión VPN"
msgstr ""
#: views_users.py:574
#: views_users.py:579
msgid ""
"Ha habido un error modificando al usuario. Los cambios no se han guardado. "
"Disculpa las molestias."
msgstr ""
#: views_users.py:679
#: views_users.py:684
msgid "Supersuario modificado con éxito"
msgstr ""
#: views_users.py:681
#: views_users.py:686
msgid ""
"Ha habido un error modificando al superusuario. Los cambios no se han "
"guardado. Disculpa las molestias."
msgstr ""
#: views_users.py:780
#: views_users.py:785
msgid "Contraseña modificada con éxito"
msgstr ""
#: views_users.py:782
#: views_users.py:787
msgid ""
"Ha habido un error modificando el postmaster. Los cambios no se han "
"guardado. Disculpa las molestias. Si el problema persiste contacta con los "
"administrador-s"
msgstr ""
#: views_users.py:799
#: views_users.py:804
msgid "Cuenta de postmaster activada con éxito"
msgstr ""
#: views_users.py:801
#: views_users.py:806
msgid "Ha habido un error al activando el postmaster. "
msgstr ""
# python
import datetime, time, random, os, urllib3, json, shutil, sys, hashlib, socket, re, psutil, binascii
import datetime, time, random, os, urllib3, json, shutil, sys, hashlib, socket, re, psutil, binascii, certifi
from distutils.dir_util import copy_tree
# django
from django.utils.translation import ugettext_lazy as _
......@@ -291,7 +291,9 @@ def get_puppet_status(request):
url = credentials.host.value + 'vmstatus/' + get_server_hostname()
if settings.DEBUG:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
http = urllib3.PoolManager()
http = urllib3.PoolManager(
cert_reqs='CERT_REQUIRED',
ca_certs=certifi.where())
# Api call for puppetstatus must be GET
response = http.request('GET', url,
headers={
......@@ -362,7 +364,9 @@ def get_release_info(request, route=None):
url = credentials.host.value + endpoint
if settings.DEBUG:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
http = urllib3.PoolManager()
http = urllib3.PoolManager(
cert_reqs='CERT_REQUIRED',
ca_certs=certifi.where())
response = http.request('POST', url,
body = json.dumps({
'vmname' : get_server_hostname(),
......
......@@ -120,7 +120,7 @@ class Activate(FormView):
# Update sudo user password and ctivate services
if 'sudopassword' in form.fields and form['sudopassword'].value():
sudopwd = form['sudopassword'].value()
services = ['sshd', 'apache', 'cron']
services = ['sshd', 'apache', 'cron', 'sudo']
sufields['userpassword'] = [(MODIFY_REPLACE, hashed(HASHED_SALTED_SHA, sudopwd))]
sufields['authorizedservice'] = [(MODIFY_REPLACE, services )]
......
# python
import urllib3, os, json, pwd
import urllib3, os, json, pwd, certifi
# django
from django.utils.translation import ugettext_lazy as _, get_language
......
......@@ -294,6 +294,7 @@ class Fqdn(FormView):
del self.request.session['session_key']
# Destroy cookies
next_page = 'changesystem'
path = settings.FORCE_SCRIPT_NAME
response = HttpResponseRedirect(reverse(next_page), locals)
response.delete_cookie('s')
response.set_cookie('oldDomain', self.servername + '.' + self.server_domain)
......@@ -308,4 +309,74 @@ class Fqdn(FormView):
class NewFqdnSet(views.View):
def get(self, request):
# TODO: if user is logged in redirect to cpanel homepage (details)
path = settings.FORCE_SCRIPT_NAME
return render(request, 'registration/fqdn-change.html', locals())
class Clean_system(views.View):
"""
View to reboot the system.
"""
def get(self, request):
try:
request.ldap.search(
settings.LDAP_TREE_BASE,
settings.LDAP_FILTERS_INSTALLED_ENABLED_SERVICES,
attributes=['ou']
)
# Get all enables service to check what can be removes
enabled_service_names = [ service.ou.value for service in request.ldap.entries ]
optionals = []
""" DOCKER """
if 'docker' in enabled_service_names:
docker_clean_message = _("Se eliminarán las imágenes de docker que no están referenciadas ni usadas por ningún contenedor."
"Si has instalado alguna imagen de docker de forma manual, sin utilizar el panel de control, asegúrate de no tender ninguna que no esté actualmente en uso y que quieras conservar.")
optionals.append(docker_clean_message)
except Exception as e:
utils.p("Clean view", "No apps installed yet. Nothing else to clean", e)
return render(request, 'pages/clean.html', locals())
@method_decorator(csrf_protect)
def post(self, request):
errors=False
"""Handle POST requests."""
try:
request.ldap.search(
settings.LDAP_TREE_CLEAN,
settings.LDAP_FILTERS_CLEAN,
attributes=['cn']
)
# if clean tree already exist in ldap update it
if self.request.ldap.entries:
try:
request.ldap.modify(
settings.LDAP_TREE_CLEAN, {
'info' : [(MODIFY_REPLACE, 'ready')],
'status' : [(MODIFY_REPLACE, 'locked')],
}
)
messages.success(request, _('Operación guardada con éxito' ))
except Exception as e:
messages.error(request, _('Se ha producido un error' ))
utils.p("Clean view", "Error updating clean entry in ldap", e)
except Exception as e:
utils.p("Clean view", "No clean entry found in ldap", e)
errors=True
if errors:
try:
# if clean tree does not exixts in ldap create it with needed value
request.ldap.add(settings.LDAP_TREE_CLEAN, [
'organizationalUnit',
'metaInfo',
'extensibleObject'
],
{'info': 'ready', 'status': 'locked'})
messages.success(request, _('Operación guardada con éxito' ))
except Exception as e:
messages.error(request, _('Se ha producido un error' ))
utils.p("Clean view", "Error creating clean entry in ldap", e)
return HttpResponseRedirect( reverse('system-details') )
......@@ -66,6 +66,11 @@ class LoginView(FormView):
'molestias. Si el problema persiste contacta con nosotras'))
return HttpResponseRedirect( reverse('login') )
elif ldap['error'] == 'LDAPInvalidCredentialsResult':
#log remote IP when login with invalid credentials
#doc: https://stackoverflow.com/questions/4581789/how-do-i-get-user-ip-address-in-django
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
ip = x_forwarded_for.split(',')[-1].strip()
print ('Bad login from: ' + ip)
messages.error(self.request, _('Las credenciales son incorrectas. ¿Las has escrito correctamente?'))
return HttpResponseRedirect( reverse('login') )
......
......@@ -18,7 +18,7 @@ LDAP_TREE_API = "ou=api,%s" % LDAP_TREE_BASE
LDAP_TREE_CPANEL = "ou=cpanel,%s" % LDAP_TREE_BASE
LDAP_TREE_REBOOT = "ou=reboot,ou=cpanel,%s" % LDAP_TREE_BASE
LDAP_TREE_DKIM = "ou=opendkim,ou=cpanel,%s" % LDAP_TREE_BASE
LDAP_TREE_CLEAN = "ou=clean,%s" % LDAP_TREE_CPANEL
# LDAP filters
LDAP_FILTERS_SFTP = "(&(objectClass=person)(!(gidnumber=27)))"
LDAP_FILTERS_USERS_WEBMASTER_FORM = "(&(objectClass=person)(uid=*)(authorizedService=sshd)(!(gidnumber=27)))"
......@@ -34,7 +34,7 @@ LDAP_FILTERS_DKIM = '(&(objectClass=organizationalUnit)(o
LDAP_FILTERS_SENDERMAIL = '(&(objectClass=organizationalUnit)(objectClass=metaInfo))'
LDAP_FILTERS_DOMAINS = '(objectClass=VirtualDomain)'
LDAP_FILTERS_MAILMAN = "(&(vd=*)(accountActive=TRUE))"
LDAP_FILTERS_CLEAN = '(&(objectClass=organizationalUnit)(objectClass=metaInfo))'
# OTHER - these are for retreiving apps logos from an external source
ASSETS_URI = "https://assets.maadix.net/"
IMAGES_FOLDER = "c-panel/images/services/"
......@@ -29,7 +29,7 @@ LOCALE_PATHS = [
#
ALLOWED_URLS = {
'admin' : [ '^(.*)$', ],
'anonymous' : [ 'login', 'password-recovery', 'password-reset', 'status-cpanel','status-cpanel-apache', 'changesystem' ],
'anonymous' : [ 'login', 'password-recovery', 'password-reset', 'status-cpanel','status-cpanel-apache', 'changesystem', 'i18n' ],
'postmaster' : [ 'login', 'mails', 'email', 'profile', 'logout' ],
'email' : [ 'login', 'email', 'profile', 'logout' ]
}
......@@ -96,6 +96,9 @@ ROOT_URLCONF = 'mxcp.urls'
# SSL
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
......@@ -127,6 +130,9 @@ AUTH_PASSWORD_VALIDATORS = [
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 10,
},
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
......@@ -134,6 +140,9 @@ AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
{'NAME': 'apps.views.auth_validators.UppercaseValidator', },
{'NAME': 'apps.views.auth_validators.LowercaseValidator', },
{'NAME': 'apps.views.auth_validators.SymbolValidator', },
]
#
......@@ -155,7 +164,7 @@ else:
# https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = 'es'
TIME_ZONE = 'UTC'
TIME_ZONE = 'Europe/Madrid'
USE_I18N = True
USE_L10N = True
USE_TZ = True
......@@ -163,10 +172,12 @@ LANGUAGES = [
('es', _('Castellano')),
('en', _('English')),
]
# Allowed hosts
#TODO: check secuirty issue with this config
# How to allow serve this app under aliases with path?
ALLOWED_HOSTS = [ '*' ]
from .custom_conf import *
""" Custom conf file included in gitignore that may be managed externally
"""
from .ldap_settings import *
......
......@@ -37,6 +37,7 @@ urlpatterns = [
path('system/reboot', system.Reboot.as_view(), name="reboot"),
path('system/update', system.Update.as_view(), name="update"),
path('system/fqdn', system.Fqdn.as_view(), name="fqdn"),
path('system/clean', system.Clean_system.as_view(), name="clean"),
path('services', services.Services.as_view(), name="services"),
# a link to services-update is harcoded inside static/mxcp/js/services-update.js
path('services/update', services.ServicesUpdate.as_view(), name="services-update"),
......
......@@ -9,3 +9,4 @@ psycopg2==2.7.5
pyasn1==0.4.4
pytz==2018.4
urllib3==1.23
certifi==2019.3.9
......@@ -4601,6 +4601,35 @@ select:hover {
max-height: 60px;
}
/* line 5, ../sass/blocks/_clean.sass */
.page--clean ul.clean-list li {
margin: 8px;
}
/* line 7, ../sass/blocks/_clean.sass */
.page--clean pre {
overflow: visible;
}
/* line 10, ../sass/blocks/_clean.sass */
.clean-button {
margin-top: 48px;
line-height: 40px;
padding: 0 12px;
border: 0;
font-family: "open-sans-condensed-bold", sans-serif;
text-transform: uppercase;
-moz-transition: background-color, ease 0.25s;
-o-transition: background-color, ease 0.25s;
-webkit-transition: background-color, ease 0.25s;
transition: background-color, ease 0.25s;
background-color: #95bb36;
color: white;
}
/* line 18, ../sass/base/reset/_3_buttons.sass */
.clean-button:hover {
background-color: #85a730;
}
/* line 1, ../sass/blocks/_dns.sass */
.dns--table {
background: white;
......@@ -6527,6 +6556,18 @@ label[for=id_home_dir] {
max-width: 120px;
}
/* line 112, ../sass/blocks/_registration-forms.sass */
#login-switch-lang {
width: 480px;
margin: 0 auto;
}