Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • Dockerfile
  • feature_invite_validuntil_minmax
  • incremental-sync
  • jwt_encode_inconsistencies
  • master
  • redis-rate-limits
  • roles-recursive-cte
  • typehints
  • v1.0.x
  • v1.1.x
  • v1.2.x
  • v1.x.x
  • v0.1.2
  • v0.1.4
  • v0.1.5
  • v0.2.0
  • v0.3.0
  • v1.0.0
  • v1.0.1
  • v1.0.2
  • v1.1.0
  • v1.1.1
  • v1.1.2
  • v1.2.0
  • v2.0.0
  • v2.0.1
  • v2.1.0
  • v2.2.0
  • v2.3.0
  • v2.3.1
30 results

Target

Select target project
  • uffd/uffd
  • rixx/uffd
  • thies/uffd
  • leona/uffd
  • strifel/uffd
  • thies/uffd-2
6 results
Select Git revision
  • ldap_user_conn_test
  • master
2 results
Show changes
Showing
with 2784 additions and 597 deletions
import secrets
import hashlib
import base64
from crypt import crypt # pylint: disable=deprecated-module
import argon2
def build_value(method_name, data):
return '{' + method_name + '}' + data
def parse_value(value):
if value is not None and value.startswith('{') and '}' in value:
method_name, data = value[1:].split('}', 1)
return method_name.lower(), data
raise ValueError('Invalid password hash')
class PasswordHashRegistry:
'''Factory for creating objects of the correct PasswordHash subclass for a
given password hash value'''
def __init__(self):
self.methods = {}
def register(self, cls):
assert cls.METHOD_NAME not in self.methods
self.methods[cls.METHOD_NAME] = cls
return cls
def parse(self, value, **kwargs):
method_name, _ = parse_value(value)
method_cls = self.methods.get(method_name)
if method_cls is None:
raise ValueError(f'Unknown password hash method {method_name}')
return method_cls(value, **kwargs)
registry = PasswordHashRegistry()
class PasswordHash:
'''OpenLDAP-/NIS-style password hash
Instances wrap password hash strings in the form "{METHOD_NAME}DATA".
Allows gradual migration of password hashing methods by checking
PasswordHash.needs_rehash every time the password is processed and rehashing
it with PasswordHash.from_password if needed. For PasswordHash.needs_rehash
to work, the PasswordHash subclass for the current password hashing method
is instantiated with target_cls set to the PasswordHash subclass of the
intended hashing method.
Instances should be created with PasswordHashRegistry.parse to get the
appropriate subclass based on the method name in a value.'''
METHOD_NAME = None
def __init__(self, value, target_cls=None):
method_name, data = parse_value(value)
if method_name != self.METHOD_NAME:
raise ValueError('Invalid password hash')
self.value = value
self.data = data
self.target_cls = target_cls or type(self)
@classmethod
def from_password(cls, password):
raise NotImplementedError()
def verify(self, password):
raise NotImplementedError()
@property
def needs_rehash(self):
return not isinstance(self, self.target_cls)
@registry.register
class PlaintextPasswordHash(PasswordHash):
'''Pseudo password hash for passwords stored without hashing
Should only be used for migration of existing plaintext passwords. Add the
prefix "{plain}" for this.'''
METHOD_NAME = 'plain'
@classmethod
def from_password(cls, password):
return cls(build_value(cls.METHOD_NAME, password))
def verify(self, password):
return secrets.compare_digest(self.data, password)
class HashlibPasswordHash(PasswordHash):
HASHLIB_ALGORITHM = None
@classmethod
def from_password(cls, password):
ctx = hashlib.new(cls.HASHLIB_ALGORITHM, password.encode())
return cls(build_value(cls.METHOD_NAME, base64.b64encode(ctx.digest()).decode()))
def verify(self, password):
digest = base64.b64decode(self.data.encode())
ctx = hashlib.new(self.HASHLIB_ALGORITHM, password.encode())
return secrets.compare_digest(digest, ctx.digest())
class SaltedHashlibPasswordHash(PasswordHash):
HASHLIB_ALGORITHM = None
@classmethod
def from_password(cls, password):
salt = secrets.token_bytes(8)
ctx = hashlib.new(cls.HASHLIB_ALGORITHM)
ctx.update(password.encode())
ctx.update(salt)
return cls(build_value(cls.METHOD_NAME, base64.b64encode(ctx.digest()+salt).decode()))
def verify(self, password):
data = base64.b64decode(self.data.encode())
ctx = hashlib.new(self.HASHLIB_ALGORITHM)
digest = data[:ctx.digest_size]
salt = data[ctx.digest_size:]
ctx.update(password.encode())
ctx.update(salt)
return secrets.compare_digest(digest, ctx.digest())
@registry.register
class MD5PasswordHash(HashlibPasswordHash):
METHOD_NAME = 'md5'
HASHLIB_ALGORITHM = 'md5'
@registry.register
class SaltedMD5PasswordHash(SaltedHashlibPasswordHash):
METHOD_NAME = 'smd5'
HASHLIB_ALGORITHM = 'md5'
@registry.register
class SHA1PasswordHash(HashlibPasswordHash):
METHOD_NAME = 'sha'
HASHLIB_ALGORITHM = 'sha1'
@registry.register
class SaltedSHA1PasswordHash(SaltedHashlibPasswordHash):
METHOD_NAME = 'ssha'
HASHLIB_ALGORITHM = 'sha1'
@registry.register
class SHA256PasswordHash(HashlibPasswordHash):
METHOD_NAME = 'sha256'
HASHLIB_ALGORITHM = 'sha256'
@registry.register
class SaltedSHA256PasswordHash(SaltedHashlibPasswordHash):
METHOD_NAME = 'ssha256'
HASHLIB_ALGORITHM = 'sha256'
@registry.register
class SHA384PasswordHash(HashlibPasswordHash):
METHOD_NAME = 'sha384'
HASHLIB_ALGORITHM = 'sha384'
@registry.register
class SaltedSHA384PasswordHash(SaltedHashlibPasswordHash):
METHOD_NAME = 'ssha384'
HASHLIB_ALGORITHM = 'sha384'
@registry.register
class SHA512PasswordHash(HashlibPasswordHash):
METHOD_NAME = 'sha512'
HASHLIB_ALGORITHM = 'sha512'
@registry.register
class SaltedSHA512PasswordHash(SaltedHashlibPasswordHash):
METHOD_NAME = 'ssha512'
HASHLIB_ALGORITHM = 'sha512'
@registry.register
class CryptPasswordHash(PasswordHash):
METHOD_NAME = 'crypt'
@classmethod
def from_password(cls, password):
return cls(build_value(cls.METHOD_NAME, crypt(password)))
def verify(self, password):
return secrets.compare_digest(crypt(password, self.data), self.data)
@registry.register
class Argon2PasswordHash(PasswordHash):
METHOD_NAME = 'argon2'
hasher = argon2.PasswordHasher()
@classmethod
def from_password(cls, password):
return cls(build_value(cls.METHOD_NAME, cls.hasher.hash(password)))
def verify(self, password):
try:
return self.hasher.verify(self.data, password)
except argon2.exceptions.Argon2Error:
return False
except argon2.exceptions.InvalidHash:
return False
@property
def needs_rehash(self):
return super().needs_rehash or self.hasher.check_needs_rehash(self.data)
class InvalidPasswordHash:
def __init__(self, value=None):
self.value = value
# pylint: disable=unused-argument
def verify(self, password):
return False
@property
def needs_rehash(self):
return True
def __bool__(self):
return False
# An alternative approach for the behaviour of PasswordHashAttribute would be
# to use sqlalchemy.TypeDecorator. A type decorator allows custom encoding and
# decoding of values coming from the database (when query results are loaded)
# and going into the database (when statements are executed).
#
# This has one downside: After setting e.g. user.password to a string value it
# remains a string value until the change is flushed. It is not possible to
# coerce values to PasswordHash objects as soon as they are set.
#
# This is too inconsistent. Code should be able to rely on user.password to
# always behave like a PasswordHash object.
class PasswordHashAttribute:
'''Descriptor for wrapping an attribute storing a password hash string
Usage example:
>>> class User:
... # Could e.g. be an SQLAlchemy.Column or just a simple attribute
... _passord_hash = None
... password = PasswordHashAttribute('_passord_hash', SHA512PasswordHash)
...
>>> user = User()
>>> type(user.password)
<class 'uffd.password_hash.InvalidPasswordHash'>
>>>
>>> user._password_hash = '{plain}my_password'
>>> type(user.password)
<class 'uffd.password_hash.InvalidPasswordHash'>
>>> user.password.needs_rehash
True
>>>
>>> user.password = 'my_password'
>>> user._passord_hash
'{sha512}3ajDRohg3LJOIoq47kQgjUPrL1/So6U4uvvTnbT/EUyYKaZL0aRxDgwCH4pBNLai+LF+zMh//nnYRZ4t8pT7AQ=='
>>> type(user.password)
<class 'uffd.password_hash.SHA512PasswordHash'>
>>>
>>> user.password = None
>>> user._passord_hash is None
True
>>> type(user.password)
<class 'uffd.password_hash.InvalidPasswordHash'>
When set to a (plaintext) password the underlying attribute is set to a hash
value for the password. When set to None the underlying attribute is also set
to None.'''
def __init__(self, attribute_name, method_cls):
self.attribute_name = attribute_name
self.method_cls = method_cls
def __get__(self, obj, objtype=None):
if obj is None:
return self
value = getattr(obj, self.attribute_name)
try:
return registry.parse(value, target_cls=self.method_cls)
except ValueError:
return InvalidPasswordHash(value)
def __set__(self, obj, value):
if value is None:
value = InvalidPasswordHash()
elif isinstance(value, str):
value = self.method_cls.from_password(value)
setattr(obj, self.attribute_name, value.value)
# Hashing method for (potentially) low entropy secrets like user passwords. Is
# usually slow and uses salting to make dictionary attacks difficult.
LowEntropyPasswordHash = Argon2PasswordHash
# Hashing method for high entropy secrets like API keys. The secrets are
# generated instead of user-selected to ensure a high level of entropy. Is
# fast and does not need salting, since dictionary attacks are not feasable
# due to high entropy.
HighEntropyPasswordHash = SHA512PasswordHash
from flask import current_app
import itsdangerous
from uffd.utils import nopad_b32decode, nopad_b32encode, nopad_urlsafe_b64decode, nopad_urlsafe_b64encode
class Remailer:
'''The remailer feature improves user privacy by hiding real mail addresses
from services and instead providing them with autogenerated pseudonymous
remailer addresses. If a service sends a mail to a remailer address, the mail
service uses an uffd API endpoint to get the real mail address and rewrites
the remailer address with it. In case of a leak of user data from a service,
the remailer addresses are useless for third-parties.
Version 2 of the remailer address format is tolerant to case conversions at
the cost of being slightly longer.'''
@property
def configured(self):
return bool(current_app.config['REMAILER_DOMAIN'])
def get_serializer(self):
secret = current_app.config['REMAILER_SECRET_KEY'] or current_app.secret_key
return itsdangerous.URLSafeSerializer(secret, salt='remailer_address_v1')
def build_v1_address(self, service_id, user_id):
payload = self.get_serializer().dumps([service_id, user_id])
return 'v1-' + payload + '@' + current_app.config['REMAILER_DOMAIN']
def build_v2_address(self, service_id, user_id):
data, sign = self.get_serializer().dumps([service_id, user_id]).split('.', 1)
data = nopad_b32encode(nopad_urlsafe_b64decode(data)).decode().lower()
sign = nopad_b32encode(nopad_urlsafe_b64decode(sign)).decode().lower()
payload = data + '-' + sign
return 'v2-' + payload + '@' + current_app.config['REMAILER_DOMAIN']
def is_remailer_domain(self, domain):
domains = {domain.lower().strip() for domain in current_app.config['REMAILER_OLD_DOMAINS']}
if current_app.config['REMAILER_DOMAIN']:
domains.add(current_app.config['REMAILER_DOMAIN'].lower().strip())
return domain.lower().strip() in domains
def parse_v1_payload(self, payload):
try:
service_id, user_id = self.get_serializer().loads(payload)
except itsdangerous.BadData:
return None
return (service_id, user_id)
def parse_v2_payload(self, payload):
data, sign = (payload.split('-', 1) + [''])[:2]
try:
data = nopad_urlsafe_b64encode(nopad_b32decode(data.upper())).decode()
sign = nopad_urlsafe_b64encode(nopad_b32decode(sign.upper())).decode()
except ValueError:
return None
payload = data + '.' + sign
try:
service_id, user_id = self.get_serializer().loads(payload)
except itsdangerous.BadData:
return None
return (service_id, user_id)
def parse_address(self, address):
if '@' not in address:
return None
local_part, domain = address.rsplit('@', 1)
if not self.is_remailer_domain(domain):
return None
prefix, payload = (local_part.strip().split('-', 1) + [''])[:2]
if prefix == 'v1':
return self.parse_v1_payload(payload)
if prefix.lower() == 'v2':
return self.parse_v2_payload(payload)
return None
remailer = Remailer()
from .views import bp as bp_ui
from .models import Role, RoleGroup, RoleUser
bp = [bp_ui]
from operator import attrgetter
from sqlalchemy import Column, String, Integer, Text, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declared_attr
from uffd.database import db
from uffd.user import User, Group
class Role(db.Model):
__tablename__ = 'role'
id = Column(Integer(), primary_key=True, autoincrement=True)
name = Column(String(32), unique=True)
description = Column(Text())
members = relationship("RoleUser", backref="role", cascade="all, delete-orphan")
groups = relationship("RoleGroup", backref="role", cascade="all, delete-orphan")
def __init__(self, name='', description=''):
self.name = name
self.description = description
def group_dns(self):
return map(attrgetter('dn'), self.groups)
def member_dns(self):
return map(attrgetter('dn'), self.members)
class LdapMapping():
id = Column(Integer(), primary_key=True, autoincrement=True)
dn = Column(String(128))
__table_args__ = (
db.UniqueConstraint('dn', 'role_id'),
)
@declared_attr
def role_id(self):
return Column(ForeignKey('role.id'))
ldapclass = None
def get_ldap(self):
return self.ldapclass.from_ldap_dn(self.dn)
def set_ldap(self, value):
self.dn = value['dn']
class RoleGroup(LdapMapping, db.Model):
__tablename__ = 'role-group'
ldapclass = User
class RoleUser(LdapMapping, db.Model):
__tablename__ = 'role-user'
ldapclass = Group
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("role.update", roleid=role.id) }}" method="POST">
<div class="align-self-center">
<div class="form-group col">
<label for="role-name">Role Name</label>
<input type="text" class="form-control" id="role-name" name="name" value="{{ role.name }}">
<small class="form-text text-muted">
</small>
</div>
<div class="form-group col">
<label for="role-description">Description</label>
<textarea class="form-control" id="role-description" name="description" rows="5">{{ role.description }}</textarea>
<small class="form-text text-muted">
</small>
</div>
<div class="form-group col">
<span>Included groups</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">name</th>
<th scope="col">description</th>
</tr>
</thead>
<tbody>
{% for group in groups|sort(attribute="name") %}
<tr id="group-{{ group.gid }}">
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="group-{{ group.gid }}-checkbox" name="group-{{ group.gid }}" value="1" aria-label="enabled" {% if group.dn in role.group_dns() %}checked{% endif %}>
</div>
</td>
<td>
<a href="{{ url_for("group.show", gid=group.gid) }}">
{{ group.name }}
</a>
</td>
<td>
{{ group.description }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="form-group col">
<p>
Members
</p>
{% for member in role.members %}
{{ member.dn }}
{% endfor %}
</div>
<div class="form-group col">
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button>
<a href="{{ url_for("role.index") }}" class="btn btn-secondary">Cancel</a>
{% if role.id %}
<a href="{{ url_for("role.delete", roleid=role.id) }}" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
{% else %}
<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
{% endif %}
</div>
</div>
</form>
{% endblock %}
from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
from uffd.navbar import register_navbar
from uffd.csrf import csrf_protect
from uffd.role.models import Role, RoleGroup
from uffd.user import Group
from uffd.session import get_current_user, login_required, is_valid_session
from uffd.database import db
bp = Blueprint("role", __name__, template_folder='templates', url_prefix='/role/')
@bp.before_request
@login_required()
def role_acl(): #pylint: disable=inconsistent-return-statements
if not role_acl_check():
flash('Access denied')
return redirect(url_for('index'))
def role_acl_check():
return is_valid_session() and get_current_user().is_in_group(current_app.config['ACL_ADMIN_GROUP'])
@bp.route("/")
@register_navbar('Roles', icon='key', blueprint=bp, visible=role_acl_check)
def index():
return render_template('role_list.html', roles=Role.query.all())
@bp.route("/<int:roleid>")
@bp.route("/new")
def show(roleid=False):
if not roleid:
role = Role()
else:
role = Role.query.get_or_404(roleid)
groups = Group.from_ldap_all()
return render_template('role.html', role=role, groups=groups)
@bp.route("/<int:roleid>/update", methods=['POST'])
@bp.route("/new", methods=['POST'])
@csrf_protect(blueprint=bp)
def update(roleid=False):
is_newrole = bool(not roleid)
session = db.session
if is_newrole:
role = Role()
session.add(role)
else:
role = session.query(Role).get_or_404(roleid)
role.name = request.values['name']
role.description = request.values['description']
groups = Group.from_ldap_all()
role_group_dns = list(role.group_dns())
for group in groups:
if request.values.get('group-{}'.format(group.gid), False):
if group.dn in role_group_dns:
continue
newmapping = RoleGroup()
newmapping.dn = group.dn
newmapping.role = role
session.add(newmapping)
elif group.dn in role_group_dns:
session.delete(RoleGroup.query.filter_by(role_id=role.id, dn=group.dn).one())
session.commit()
return redirect(url_for('role.index'))
@bp.route("/<int:roleid>/del")
@csrf_protect(blueprint=bp)
def delete(roleid):
session = db.session
role = session.query(Role).get_or_404(roleid)
session.delete(role)
session.commit()
return redirect(url_for('role.index'))
from flask import redirect
def secure_local_redirect(target):
# Reject URLs that include a scheme or host part
if not target.startswith('/') or target.startswith('//'):
target = '/'
return redirect(target)
from .views import bp as bp_ui, send_passwordreset
bp = [bp_ui]
import datetime
import secrets
from sqlalchemy import Column, String, DateTime
from uffd.database import db
def random_token():
return secrets.token_hex(128)
class Token():
token = Column(String(128), primary_key=True, default=random_token)
created = Column(DateTime, default=datetime.datetime.now)
class PasswordToken(Token, db.Model):
__tablename__ = 'passwordToken'
loginname = Column(String(32))
class MailToken(Token, db.Model):
__tablename__ = 'mailToken'
loginname = Column(String(32))
newmail = Column(String(255))
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("selfservice.forgot_password") }}" method="POST">
<div class="row mt-2 justify-content-center">
<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;">
<div class="text-center">
<img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" >
</div>
<div class="col-12">
<h2 class="text-center">Forgot password</h2>
</div>
<div class="form-group col-12">
<label for="user-loginname">Login Name</label>
<input type="text" class="form-control" id="user-loginname" name="loginname" required="required" tabindex = "1">
</div>
<div class="form-group col-12">
<label for="user-mail">Mail Address</label>
<input type="text" class="form-control" id="user-mail" name="mail" required="required" tabindex = "2">
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex = "3">Send password reset mail</button>
</div>
</div>
</div>
</form>
{% endblock %}
Hi {{ user.displayname }},
you have requested to change your mail address.
To confirm the change, please visit this URL: {{ url_for('selfservice.token_mail', token=token, _external=True) }}
**Please note this link is only valid for 48h**
Kind regards,
uffd
Hi {{ user.displayname }},
you have requested a password reset.
To reset your password, visit this url: {{ url_for('selfservice.self_token_password', token=token, _external=True) }}
**Please note this link is only valid for 48h**
If you did not request a password reset, you do not need to do anything.
Kind regards,
uffd
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("selfservice.update") }}" method="POST" onInput="
password2.setCustomValidity(password1.value != password2.value ? 'Passwords do not match.' : '');
password1.setCustomValidity((password1.value.length < 8 || password1.value.length == 0) ? 'Password is too short' : '') ">
<div class="align-self-center row">
<div class="form-group col-md-6">
<label for="user-uid">Uid</label>
<input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.uid }}" readonly>
</div>
<div class="form-group col-md-6">
<label for="user-loginname">Login Name</label>
<input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname }}" readonly>
</div>
<div class="form-group col-md-6">
<label for="user-displayname">Display Name</label>
<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname }}">
</div>
<div class="form-group col-md-6">
<label for="user-mail">Mail</label>
<input type="email" class="form-control" id="user-mail" name="mail" value="{{ user.mail }}">
<small class="form-text text-muted">
We will sent you a confirmation mail to set a new mail address.
</small>
</div>
<div class="form-group col-md-6">
<label for="user-password1">Password</label>
<input type="password" class="form-control" id="user-password1" name="password1" placeholder="●●●●●●●●">
<small class="form-text text-muted">
At least 8 characters, no other special requirements. But please don't be stupid and use a password manager.
</small>
</div>
<div class="form-group col-md-6">
<label for="user-password2">Password Repeat</label>
<input type="password" class="form-control" id="user-password2" name="password2" placeholder="●●●●●●●●">
</div>
<div class="form-group col-md-12">
<button type="submit" class="btn btn-primary float-right"><i class="fa fa-save" aria-hidden="true"></i> Save</button>
</div>
</div>
</form>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("selfservice.token_password", token=token) }}" method="POST" onInput="password2.setCustomValidity(password1.value != password2.value ? 'Passwords do not match.' : '') ">
<div class="row mt-2 justify-content-center">
<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;">
<div class="text-center">
<img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" >
</div>
<div class="col-12">
<h2 class="text-center">Reset password</h2>
</div>
<div class="form-group col-12">
<label for="user-password1">New Password</label>
<input type="password" class="form-control" id="user-password1" name="password1" required="required" tabindex = "2">
<small class="form-text text-muted">
At least 8 characters, no other special requirements. But please don't be stupid and use a password manager.
</small>
</div>
<div class="form-group col-12">
<label for="user-password2">Repeat Password</label>
<input type="password" class="form-control" id="user-password2" name="password2" required="required" tabindex = "2">
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex = "3">Set password</button>
</div>
</div>
</div>
</form>
{% endblock %}
import datetime
import smtplib
from email.message import EmailMessage
import email.utils
from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
from uffd.navbar import register_navbar
from uffd.csrf import csrf_protect
from uffd.user.models import User
from uffd.session import get_current_user, login_required, is_valid_session
from uffd.ldap import loginname_to_dn
from uffd.selfservice.models import PasswordToken, MailToken
from uffd.database import db
bp = Blueprint("selfservice", __name__, template_folder='templates', url_prefix='/self/')
@bp.route("/")
@register_navbar('Selfservice', icon='portrait', blueprint=bp, visible=is_valid_session)
@login_required()
def index():
return render_template('self.html', user=get_current_user())
@bp.route("/update", methods=(['POST']))
@csrf_protect(blueprint=bp)
@login_required()
def update():
user = get_current_user()
if request.values['displayname'] != user.displayname:
if user.set_displayname(request.values['displayname']):
flash('Display name changed.')
else:
flash('Display name is not valid.')
if request.values['password1']:
if not request.values['password1'] == request.values['password2']:
flash('Passwords do not match')
else:
if user.set_password(request.values['password1']):
flash('Password changed.')
else:
flash('Password could not be set.')
if request.values['mail'] != user.mail:
send_mail_verification(user.loginname, request.values['mail'])
flash('We sent you an email, please verify your mail address.')
user.to_ldap()
return redirect(url_for('selfservice.index'))
@bp.route("/passwordreset", methods=(['GET', 'POST']))
def forgot_password():
if request.method == 'GET':
return render_template('forgot_password.html')
loginname = request.values['loginname']
mail = request.values['mail']
flash("We sent a mail to this users mail address if you entered the correct mail and login name combination")
user = User.from_ldap_dn(loginname_to_dn(loginname))
if user and user.mail == mail:
send_passwordreset(loginname)
return redirect(url_for('session.login'))
@bp.route("/token/password/<token>", methods=(['POST', 'GET']))
def token_password(token):
session = db.session
dbtoken = PasswordToken.query.get(token)
if not dbtoken or dbtoken.created < (datetime.datetime.now() - datetime.timedelta(days=2)):
flash('Token expired, please try again.')
if dbtoken:
session.delete(dbtoken)
session.commit()
return redirect(url_for('session.login'))
if request.method == 'GET':
return render_template('set_password.html', token=token)
if not request.values['password1']:
flash('You need to set a password, please try again.')
return render_template('set_password.html', token=token)
if not request.values['password1'] == request.values['password2']:
flash('Passwords do not match, please try again.')
return render_template('set_password.html', token=token)
user = User.from_ldap_dn(loginname_to_dn(dbtoken.loginname))
if not user.set_password(request.values['password1']):
flash('Password ist not valid, please try again.')
return render_template('set_password.html', token=token)
user.to_ldap()
flash('New password set')
session.delete(dbtoken)
session.commit()
return redirect(url_for('session.login'))
@bp.route("/token/mail_verification/<token>")
@login_required()
def token_mail(token):
session = db.session
dbtoken = MailToken.query.get(token)
if not dbtoken or dbtoken.created < (datetime.datetime.now() - datetime.timedelta(days=2)):
flash('Token expired, please try again.')
if dbtoken:
session.delete(dbtoken)
session.commit()
return redirect(url_for('selfservice.index'))
user = User.from_ldap_dn(loginname_to_dn(dbtoken.loginname))
user.set_mail(dbtoken.newmail)
user.to_ldap()
flash('New mail set')
session.delete(dbtoken)
session.commit()
return redirect(url_for('selfservice.index'))
def send_mail_verification(loginname, newmail):
session = db.session
expired_tokens = MailToken.query.filter(MailToken.created < (datetime.datetime.now() - datetime.timedelta(days=2))).all()
for i in expired_tokens:
session.delete(i)
token = MailToken()
token.loginname = loginname
token.newmail = newmail
session.add(token)
session.commit()
user = User.from_ldap_dn(loginname_to_dn(loginname))
msg = EmailMessage()
msg.set_content(render_template('mailverification.mail.txt', user=user, token=token.token))
msg['Subject'] = 'Mail verification'
send_mail(newmail, msg)
def send_passwordreset(loginname):
session = db.session
expired_tokens = PasswordToken.query.filter(PasswordToken.created < (datetime.datetime.now() - datetime.timedelta(days=2))).all()
for i in expired_tokens:
session.delete(i)
token = PasswordToken()
token.loginname = loginname
session.add(token)
session.commit()
user = User.from_ldap_dn(loginname_to_dn(loginname))
msg = EmailMessage()
msg.set_content(render_template('passwordreset.mail.txt', user=user, token=token.token))
msg['Subject'] = 'Password reset'
send_mail(user.mail, msg)
def send_mail(to_address, msg):
server = smtplib.SMTP(host=current_app.config['MAIL_SERVER'], port=current_app.config['MAIL_PORT'])
if current_app.config['MAIL_USE_STARTTLS']:
server.starttls()
server.login(current_app.config['MAIL_USERNAME'], current_app.config['MAIL_PASSWORD'])
msg['From'] = current_app.config['MAIL_FROM_ADDRESS']
msg['To'] = to_address
msg['Date'] = email.utils.formatdate(localtime=1)
msg['Message-ID'] = email.utils.make_msgid()
server.send_message(msg)
server.quit()
import smtplib
from email.message import EmailMessage
import email.utils
from flask import render_template, current_app
def sendmail(addr, subject, template_name, **kwargs):
msg = EmailMessage()
msg.set_content(render_template(template_name, **kwargs))
msg['Subject'] = subject
msg['From'] = current_app.config['MAIL_FROM_ADDRESS']
msg['To'] = addr
msg['Date'] = email.utils.formatdate(localtime=1)
msg['Message-ID'] = email.utils.make_msgid()
try:
if current_app.debug:
current_app.last_mail = None
current_app.logger.debug('Trying to send email to %s:\n'%(addr)+str(msg))
if current_app.debug and current_app.config.get('MAIL_SKIP_SEND', False):
if current_app.config['MAIL_SKIP_SEND'] == 'fail':
raise smtplib.SMTPException()
current_app.last_mail = msg
return True
server = smtplib.SMTP(host=current_app.config['MAIL_SERVER'], port=current_app.config['MAIL_PORT'])
if current_app.config['MAIL_USE_STARTTLS']:
server.starttls()
if current_app.config['MAIL_USERNAME']:
server.login(current_app.config['MAIL_USERNAME'], current_app.config['MAIL_PASSWORD'])
server.send_message(msg)
server.quit()
if current_app.debug:
current_app.last_mail = msg
return True
except smtplib.SMTPException:
return False
from .views import bp as bp_ui, get_current_user, login_required, is_valid_session
bp = [bp_ui]
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("session.login", ref=ref) }}" method="POST">
<div class="row mt-2 justify-content-center">
<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;">
<div class="text-center">
<img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" >
</div>
<div class="col-12">
<h2 class="text-center">Login</h2>
</div>
<div class="form-group col-12">
<label for="user-loginname">Login Name</label>
<input type="text" class="form-control" id="user-loginname" name="loginname" required="required" tabindex = "1">
</div>
<div class="form-group col-12">
<label for="user-password1">Password</label>
<input type="password" class="form-control" id="user-password1" name="password" required="required" tabindex = "2">
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex = "3">Login</button>
</div>
<div class="clearfix col-12">
<a href="#" class="float-left">Register</a>
<a href="{{ url_for("selfservice.forgot_password") }}" class="float-right">Forgot Password?</a>
</div>
</div>
</div>
</form>
{% endblock %}
import datetime
import secrets
import functools
from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session
from uffd.user.models import User
from uffd.ldap import user_conn, uid_to_dn
bp = Blueprint("session", __name__, template_folder='templates', url_prefix='/')
@bp.route("/logout")
def logout():
session.clear()
return redirect(url_for('.login'))
@bp.route("/login", methods=('GET', 'POST'))
def login():
if request.method == 'GET':
return render_template('login.html', ref=request.values.get('ref'))
username = request.form['loginname']
password = request.form['password']
conn = user_conn(username, password)
if not conn:
flash('Login name or password is wrong')
return redirect(url_for('.login'))
conn.search(conn.user, '(objectClass=person)')
if not len(conn.entries) == 1:
flash('Login name or password is wrong')
return render_template('login.html', ref=request.values.get('ref'))
user = User.from_ldap(conn.entries[0])
if not user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP']):
flash('You do not have access to this service')
return render_template('login.html', ref=request.values.get('ref'))
session['user_uid'] = user.uid
session['logintime'] = datetime.datetime.now().timestamp()
session['_csrf_token'] = secrets.token_hex(128)
return redirect(request.values.get('ref', url_for('index')))
def get_current_user():
if not session.get('user_uid'):
return None
if not hasattr(request, 'current_user'):
request.current_user = User.from_ldap_dn(uid_to_dn(session['user_uid']))
return request.current_user
def is_valid_session():
user = get_current_user()
if not user:
return False
if datetime.datetime.now().timestamp() > session['logintime'] + current_app.config['SESSION_LIFETIME_SECONDS']:
return False
return True
bp.add_app_template_global(is_valid_session)
def login_required(group=None):
def wrapper(func):
@functools.wraps(func)
def decorator(*args, **kwargs):
if not is_valid_session():
flash('You need to login first')
return redirect(url_for('session.login', ref=request.url))
if not get_current_user().is_in_group(group):
flash('Access denied')
return redirect(url_for('index'))
return func(*args, **kwargs)
return decorator
return wrapper
/*!
* Bootstrap-Dark v4.0.0 (https://github.com/ForEvolve/bootstrap-dark)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
@media (prefers-color-scheme:dark) {
body {
color: #d3d3d3;
background-color: #191d21
}
abbr[data-original-title],
abbr[title] {
border-bottom: 0
}
a {
color: #adadad;
background-color: transparent
}
a:hover {
color: #878787
}
a:not([href]):not([class]) {
color: inherit
}
a:not([href]):not([class]):hover {
color: inherit
}
caption {
color: #6c757d
}
fieldset {
border: 0
}
legend {
color: inherit
}
hr {
border: 0;
border-top: 1px solid rgba(255,255,255,.1)
}
.mark,
mark {
background-color: #fcf8e3
}
.blockquote-footer {
color: #6c757d
}
.img-thumbnail {
background-color: #fff;
border: 1px solid #dee2e6
}
.figure-caption {
color: #6c757d
}
code {
color: #e83e8c
}
a > code {
color: inherit
}
kbd {
color: #fff;
background-color: #212529
}
pre {
color: #f8f9fa
}
pre code {
color: inherit
}
}
@media (prefers-color-scheme:dark) {
.table {
color: #d3d3d3
}
.table td,
.table th {
border-top: 1px solid #343a40
}
.table thead th {
border-bottom: 2px solid #343a40
}
.table tbody + tbody {
border-top: 2px solid #343a40
}
.table-bordered {
border: 1px solid #343a40
}
.table-bordered td,
.table-bordered th {
border: 1px solid #343a40
}
.table-borderless tbody + tbody,
.table-borderless td,
.table-borderless th,
.table-borderless thead th {
border: 0
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0,0,0,.05)
}
.table-hover tbody tr:hover {
color: #d3d3d3;
background-color: rgba(0,0,0,.075)
}
.table-primary,
.table-primary > td,
.table-primary > th {
background-color: #c6e1ff
}
.table-primary tbody + tbody,
.table-primary td,
.table-primary th,
.table-primary thead th {
border-color: #95c8ff
}
.table-hover .table-primary:hover {
background-color: #add4ff
}
.table-hover .table-primary:hover > td,
.table-hover .table-primary:hover > th {
background-color: #add4ff
}
.table-secondary,
.table-secondary > td,
.table-secondary > th {
background-color: #d6d8db
}
.table-secondary tbody + tbody,
.table-secondary td,
.table-secondary th,
.table-secondary thead th {
border-color: #b3b7bb
}
.table-hover .table-secondary:hover {
background-color: #c8cbcf
}
.table-hover .table-secondary:hover > td,
.table-hover .table-secondary:hover > th {
background-color: #c8cbcf
}
.table-success,
.table-success > td,
.table-success > th {
background-color: #c3e6cb
}
.table-success tbody + tbody,
.table-success td,
.table-success th,
.table-success thead th {
border-color: #8fd19e
}
.table-hover .table-success:hover {
background-color: #b1dfbb
}
.table-hover .table-success:hover > td,
.table-hover .table-success:hover > th {
background-color: #b1dfbb
}
.table-info,
.table-info > td,
.table-info > th {
background-color: #bee5eb
}
.table-info tbody + tbody,
.table-info td,
.table-info th,
.table-info thead th {
border-color: #86cfda
}
.table-hover .table-info:hover {
background-color: #abdde5
}
.table-hover .table-info:hover > td,
.table-hover .table-info:hover > th {
background-color: #abdde5
}
.table-warning,
.table-warning > td,
.table-warning > th {
background-color: #fedbbd
}
.table-warning tbody + tbody,
.table-warning td,
.table-warning th,
.table-warning thead th {
border-color: #febc85
}
.table-hover .table-warning:hover {
background-color: #fecda4
}
.table-hover .table-warning:hover > td,
.table-hover .table-warning:hover > th {
background-color: #fecda4
}
.table-danger,
.table-danger > td,
.table-danger > th {
background-color: #f5c6cb
}
.table-danger tbody + tbody,
.table-danger td,
.table-danger th,
.table-danger thead th {
border-color: #ed969e
}
.table-hover .table-danger:hover {
background-color: #f1b0b7
}
.table-hover .table-danger:hover > td,
.table-hover .table-danger:hover > th {
background-color: #f1b0b7
}
.table-light,
.table-light > td,
.table-light > th {
background-color: #f6f7f8
}
.table-light tbody + tbody,
.table-light td,
.table-light th,
.table-light thead th {
border-color: #eef0f2
}
.table-hover .table-light:hover {
background-color: #e8eaed
}
.table-hover .table-light:hover > td,
.table-hover .table-light:hover > th {
background-color: #e8eaed
}
.table-dark,
.table-dark > td,
.table-dark > th {
background-color: #c6c8ca
}
.table-dark tbody + tbody,
.table-dark td,
.table-dark th,
.table-dark thead th {
border-color: #95999c
}
.table-hover .table-dark:hover {
background-color: #b9bbbe
}
.table-hover .table-dark:hover > td,
.table-hover .table-dark:hover > th {
background-color: #b9bbbe
}
.table-active,
.table-active > td,
.table-active > th {
background-color: rgba(0,0,0,.075)
}
.table-hover .table-active:hover {
background-color: rgba(0,0,0,.075)
}
.table-hover .table-active:hover > td,
.table-hover .table-active:hover > th {
background-color: rgba(0,0,0,.075)
}
.table .thead-dark th {
color: #dee2e6;
background-color: #343a40;
border-color: #454d55
}
.table .thead-light th {
color: #495057;
background-color: #e9ecef;
border-color: #343a40
}
.table-dark {
color: #dee2e6;
background-color: #343a40
}
.table-dark td,
.table-dark th,
.table-dark thead th {
border-color: #454d55
}
.table-dark.table-bordered {
border: 0
}
.table-dark.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(255,255,255,.05)
}
.table-dark.table-hover tbody tr:hover {
color: #fff;
background-color: rgba(255,255,255,.075)
}
}
@media (prefers-color-scheme:dark) and (max-width:575.98px) {
.table-responsive-sm > .table-bordered {
border: 0
}
}
@media (prefers-color-scheme:dark) and (max-width:767.98px) {
.table-responsive-md > .table-bordered {
border: 0
}
}
@media (prefers-color-scheme:dark) and (max-width:991.98px) {
.table-responsive-lg > .table-bordered {
border: 0
}
}
@media (prefers-color-scheme:dark) and (max-width:1199.98px) {
.table-responsive-xl > .table-bordered {
border: 0
}
}
@media (prefers-color-scheme:dark) {
.table-responsive > .table-bordered {
border: 0
}
.table-primary,
.table-primary > td,
.table-primary > th {
color: #343a40
}
.table-hover .table-primary:hover {
color: #343a40
}
.table-hover .table-primary:hover > td,
.table-hover .table-primary:hover > th {
color: #343a40
}
.table-secondary,
.table-secondary > td,
.table-secondary > th {
color: #343a40
}
.table-hover .table-secondary:hover {
color: #343a40
}
.table-hover .table-secondary:hover > td,
.table-hover .table-secondary:hover > th {
color: #343a40
}
.table-success,
.table-success > td,
.table-success > th {
color: #343a40
}
.table-hover .table-success:hover {
color: #343a40
}
.table-hover .table-success:hover > td,
.table-hover .table-success:hover > th {
color: #343a40
}
.table-info,
.table-info > td,
.table-info > th {
color: #343a40
}
.table-hover .table-info:hover {
color: #343a40
}
.table-hover .table-info:hover > td,
.table-hover .table-info:hover > th {
color: #343a40
}
.table-warning,
.table-warning > td,
.table-warning > th {
color: #343a40
}
.table-hover .table-warning:hover {
color: #343a40
}
.table-hover .table-warning:hover > td,
.table-hover .table-warning:hover > th {
color: #343a40
}
.table-danger,
.table-danger > td,
.table-danger > th {
color: #343a40
}
.table-hover .table-danger:hover {
color: #343a40
}
.table-hover .table-danger:hover > td,
.table-hover .table-danger:hover > th {
color: #343a40
}
.table-light,
.table-light > td,
.table-light > th {
color: #343a40
}
.table-hover .table-light:hover {
color: #343a40
}
.table-hover .table-light:hover > td,
.table-hover .table-light:hover > th {
color: #343a40
}
.table-dark,
.table-dark > td,
.table-dark > th {
color: #343a40
}
.table-hover .table-dark:hover {
color: #343a40
}
.table-hover .table-dark:hover > td,
.table-hover .table-dark:hover > th {
color: #343a40
}
.table-active,
.table-active > td,
.table-active > th {
color: #e9ecef
}
.table-hover .table-active:hover {
color: #e9ecef
}
.table-hover .table-active:hover > td,
.table-hover .table-active:hover > th {
color: #e9ecef
}
.table-dark {
color: #dee2e6
}
.form-control {
color: #dee2e6;
background-color: #000;
border: 1px solid #6c757d
}
}
@media (prefers-color-scheme:dark) {
.form-control::-ms-expand {
background-color: transparent;
border: 0
}
.form-control:focus {
color: #dee2e6;
background-color: #191d21;
border-color: #b3d7ff;
box-shadow: 0 0 0 .2rem rgba(0,123,255,.25)
}
.form-control::-webkit-input-placeholder {
color: #6c757d
}
.form-control::-moz-placeholder {
color: #6c757d
}
.form-control::-ms-input-placeholder {
color: #6c757d
}
.form-control::placeholder {
color: #6c757d
}
.form-control:disabled,
.form-control[readonly] {
background-color: #343a40
}
select.form-control:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 #dee2e6
}
select.form-control:focus::-ms-value {
color: #dee2e6;
background-color: #000
}
.form-control-plaintext {
color: #212529;
background-color: transparent;
border: solid transparent
}
.form-check-input:disabled ~ .form-check-label,
.form-check-input[disabled] ~ .form-check-label {
color: #6c757d
}
.valid-feedback {
color: #28a745
}
.valid-tooltip {
color: #e9ecef;
background-color: rgba(40,167,69,.9)
}
.form-control.is-valid,
.was-validated .form-control:valid {
border-color: #28a745;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e")
}
.form-control.is-valid:focus,
.was-validated .form-control:valid:focus {
border-color: #28a745;
box-shadow: 0 0 0 .2rem rgba(40,167,69,.25)
}
.custom-select.is-valid,
.was-validated .custom-select:valid {
border-color: #28a745;
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#000 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat
}
.custom-select.is-valid:focus,
.was-validated .custom-select:valid:focus {
border-color: #28a745;
box-shadow: 0 0 0 .2rem rgba(40,167,69,.25)
}
.form-check-input.is-valid ~ .form-check-label,
.was-validated .form-check-input:valid ~ .form-check-label {
color: #28a745
}
.custom-control-input.is-valid ~ .custom-control-label,
.was-validated .custom-control-input:valid ~ .custom-control-label {
color: #28a745
}
.custom-control-input.is-valid ~ .custom-control-label::before,
.was-validated .custom-control-input:valid ~ .custom-control-label::before {
border-color: #28a745
}
.custom-control-input.is-valid:checked ~ .custom-control-label::before,
.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before {
border-color: #34ce57;
background-color: #34ce57
}
.custom-control-input.is-valid:focus ~ .custom-control-label::before,
.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before {
box-shadow: 0 0 0 .2rem rgba(40,167,69,.25)
}
.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before,
.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before {
border-color: #28a745
}
.custom-file-input.is-valid ~ .custom-file-label,
.was-validated .custom-file-input:valid ~ .custom-file-label {
border-color: #28a745
}
.custom-file-input.is-valid:focus ~ .custom-file-label,
.was-validated .custom-file-input:valid:focus ~ .custom-file-label {
border-color: #28a745;
box-shadow: 0 0 0 .2rem rgba(40,167,69,.25)
}
.invalid-feedback {
color: #dc3545
}
.invalid-tooltip {
color: #e9ecef;
background-color: rgba(220,53,69,.9)
}
.form-control.is-invalid,
.was-validated .form-control:invalid {
border-color: #dc3545;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e")
}
.form-control.is-invalid:focus,
.was-validated .form-control:invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 .2rem rgba(220,53,69,.25)
}
.custom-select.is-invalid,
.was-validated .custom-select:invalid {
border-color: #dc3545;
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#000 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat
}
.custom-select.is-invalid:focus,
.was-validated .custom-select:invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 .2rem rgba(220,53,69,.25)
}
.form-check-input.is-invalid ~ .form-check-label,
.was-validated .form-check-input:invalid ~ .form-check-label {
color: #dc3545
}
.custom-control-input.is-invalid ~ .custom-control-label,
.was-validated .custom-control-input:invalid ~ .custom-control-label {
color: #dc3545
}
.custom-control-input.is-invalid ~ .custom-control-label::before,
.was-validated .custom-control-input:invalid ~ .custom-control-label::before {
border-color: #dc3545
}
.custom-control-input.is-invalid:checked ~ .custom-control-label::before,
.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before {
border-color: #e4606d;
background-color: #e4606d
}
.custom-control-input.is-invalid:focus ~ .custom-control-label::before,
.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before {
box-shadow: 0 0 0 .2rem rgba(220,53,69,.25)
}
.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before,
.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before {
border-color: #dc3545
}
.custom-file-input.is-invalid ~ .custom-file-label,
.was-validated .custom-file-input:invalid ~ .custom-file-label {
border-color: #dc3545
}
.custom-file-input.is-invalid:focus ~ .custom-file-label,
.was-validated .custom-file-input:invalid:focus ~ .custom-file-label {
border-color: #dc3545;
box-shadow: 0 0 0 .2rem rgba(220,53,69,.25)
}
}
@media (prefers-color-scheme:dark) {
.btn {
color: #d3d3d3;
background-color: transparent;
border: 1px solid transparent
}
}
@media (prefers-color-scheme:dark) {
.btn:hover {
color: #d3d3d3
}
.btn.focus,
.btn:focus {
box-shadow: 0 0 0 .2rem rgba(0,123,255,.25)
}
.btn-primary {
color: #e9ecef;
background-color: #3395ff;
border-color: #3395ff
}
.btn-primary:hover {
color: #e9ecef;
background-color: #0d82ff;
border-color: #007bff
}
.btn-primary.focus,
.btn-primary:focus {
color: #e9ecef;
background-color: #0d82ff;
border-color: #007bff;
box-shadow: 0 0 0 .2rem rgba(78,162,253,.5)
}
.btn-primary.disabled,
.btn-primary:disabled {
color: #e9ecef;
background-color: #3395ff;
border-color: #3395ff
}
.btn-primary:not(:disabled):not(.disabled).active,
.btn-primary:not(:disabled):not(.disabled):active,
.show > .btn-primary.dropdown-toggle {
color: #e9ecef;
background-color: #007bff;
border-color: #0075f2
}
.btn-primary:not(:disabled):not(.disabled).active:focus,
.btn-primary:not(:disabled):not(.disabled):active:focus,
.show > .btn-primary.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(78,162,253,.5)
}
.btn-secondary {
color: #e9ecef;
background-color: #6c757d;
border-color: #6c757d
}
.btn-secondary:hover {
color: #e9ecef;
background-color: #5a6268;
border-color: #545b62
}
.btn-secondary.focus,
.btn-secondary:focus {
color: #e9ecef;
background-color: #5a6268;
border-color: #545b62;
box-shadow: 0 0 0 .2rem rgba(127,135,142,.5)
}
.btn-secondary.disabled,
.btn-secondary:disabled {
color: #e9ecef;
background-color: #6c757d;
border-color: #6c757d
}
.btn-secondary:not(:disabled):not(.disabled).active,
.btn-secondary:not(:disabled):not(.disabled):active,
.show > .btn-secondary.dropdown-toggle {
color: #e9ecef;
background-color: #545b62;
border-color: #4e555b
}
.btn-secondary:not(:disabled):not(.disabled).active:focus,
.btn-secondary:not(:disabled):not(.disabled):active:focus,
.show > .btn-secondary.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(127,135,142,.5)
}
.btn-success {
color: #e9ecef;
background-color: #28a745;
border-color: #28a745
}
.btn-success:hover {
color: #e9ecef;
background-color: #218838;
border-color: #1e7e34
}
.btn-success.focus,
.btn-success:focus {
color: #e9ecef;
background-color: #218838;
border-color: #1e7e34;
box-shadow: 0 0 0 .2rem rgba(69,177,95,.5)
}
.btn-success.disabled,
.btn-success:disabled {
color: #e9ecef;
background-color: #28a745;
border-color: #28a745
}
.btn-success:not(:disabled):not(.disabled).active,
.btn-success:not(:disabled):not(.disabled):active,
.show > .btn-success.dropdown-toggle {
color: #e9ecef;
background-color: #1e7e34;
border-color: #1c7430
}
.btn-success:not(:disabled):not(.disabled).active:focus,
.btn-success:not(:disabled):not(.disabled):active:focus,
.show > .btn-success.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(69,177,95,.5)
}
.btn-info {
color: #e9ecef;
background-color: #17a2b8;
border-color: #17a2b8
}
.btn-info:hover {
color: #e9ecef;
background-color: #138496;
border-color: #117a8b
}
.btn-info.focus,
.btn-info:focus {
color: #e9ecef;
background-color: #138496;
border-color: #117a8b;
box-shadow: 0 0 0 .2rem rgba(55,173,192,.5)
}
.btn-info.disabled,
.btn-info:disabled {
color: #e9ecef;
background-color: #17a2b8;
border-color: #17a2b8
}
.btn-info:not(:disabled):not(.disabled).active,
.btn-info:not(:disabled):not(.disabled):active,
.show > .btn-info.dropdown-toggle {
color: #e9ecef;
background-color: #117a8b;
border-color: #10707f
}
.btn-info:not(:disabled):not(.disabled).active:focus,
.btn-info:not(:disabled):not(.disabled):active:focus,
.show > .btn-info.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(55,173,192,.5)
}
.btn-warning {
color: #343a40;
background-color: #fd7e14;
border-color: #fd7e14
}
.btn-warning:hover {
color: #e9ecef;
background-color: #e96b02;
border-color: #dc6502
}
.btn-warning.focus,
.btn-warning:focus {
color: #e9ecef;
background-color: #e96b02;
border-color: #dc6502;
box-shadow: 0 0 0 .2rem rgba(223,116,27,.5)
}
.btn-warning.disabled,
.btn-warning:disabled {
color: #343a40;
background-color: #fd7e14;
border-color: #fd7e14
}
.btn-warning:not(:disabled):not(.disabled).active,
.btn-warning:not(:disabled):not(.disabled):active,
.show > .btn-warning.dropdown-toggle {
color: #e9ecef;
background-color: #dc6502;
border-color: #cf5f02
}
.btn-warning:not(:disabled):not(.disabled).active:focus,
.btn-warning:not(:disabled):not(.disabled):active:focus,
.show > .btn-warning.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(223,116,27,.5)
}
.btn-danger {
color: #e9ecef;
background-color: #dc3545;
border-color: #dc3545
}
.btn-danger:hover {
color: #e9ecef;
background-color: #c82333;
border-color: #bd2130
}
.btn-danger.focus,
.btn-danger:focus {
color: #e9ecef;
background-color: #c82333;
border-color: #bd2130;
box-shadow: 0 0 0 .2rem rgba(222,80,95,.5)
}
.btn-danger.disabled,
.btn-danger:disabled {
color: #e9ecef;
background-color: #dc3545;
border-color: #dc3545
}
.btn-danger:not(:disabled):not(.disabled).active,
.btn-danger:not(:disabled):not(.disabled):active,
.show > .btn-danger.dropdown-toggle {
color: #e9ecef;
background-color: #bd2130;
border-color: #b21f2d
}
.btn-danger:not(:disabled):not(.disabled).active:focus,
.btn-danger:not(:disabled):not(.disabled):active:focus,
.show > .btn-danger.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(222,80,95,.5)
}
.btn-light {
color: #343a40;
background-color: #dee2e6;
border-color: #dee2e6
}
.btn-light:hover {
color: #343a40;
background-color: #c8cfd6;
border-color: #c1c9d0
}
.btn-light.focus,
.btn-light:focus {
color: #343a40;
background-color: #c8cfd6;
border-color: #c1c9d0;
box-shadow: 0 0 0 .2rem rgba(197,201,205,.5)
}
.btn-light.disabled,
.btn-light:disabled {
color: #343a40;
background-color: #dee2e6;
border-color: #dee2e6
}
.btn-light:not(:disabled):not(.disabled).active,
.btn-light:not(:disabled):not(.disabled):active,
.show > .btn-light.dropdown-toggle {
color: #343a40;
background-color: #c1c9d0;
border-color: #bac2cb
}
.btn-light:not(:disabled):not(.disabled).active:focus,
.btn-light:not(:disabled):not(.disabled):active:focus,
.show > .btn-light.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(197,201,205,.5)
}
.btn-dark {
color: #e9ecef;
background-color: #343a40;
border-color: #343a40
}
.btn-dark:hover {
color: #e9ecef;
background-color: #23272b;
border-color: #1d2124
}
.btn-dark.focus,
.btn-dark:focus {
color: #e9ecef;
background-color: #23272b;
border-color: #1d2124;
box-shadow: 0 0 0 .2rem rgba(79,85,90,.5)
}
.btn-dark.disabled,
.btn-dark:disabled {
color: #e9ecef;
background-color: #343a40;
border-color: #343a40
}
.btn-dark:not(:disabled):not(.disabled).active,
.btn-dark:not(:disabled):not(.disabled):active,
.show > .btn-dark.dropdown-toggle {
color: #e9ecef;
background-color: #1d2124;
border-color: #171a1d
}
.btn-dark:not(:disabled):not(.disabled).active:focus,
.btn-dark:not(:disabled):not(.disabled):active:focus,
.show > .btn-dark.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(79,85,90,.5)
}
.btn-outline-primary {
color: #3395ff;
border-color: #3395ff
}
.btn-outline-primary:hover {
color: #e9ecef;
background-color: #3395ff;
border-color: #3395ff
}
.btn-outline-primary.focus,
.btn-outline-primary:focus {
box-shadow: 0 0 0 .2rem rgba(51,149,255,.5)
}
.btn-outline-primary.disabled,
.btn-outline-primary:disabled {
color: #3395ff;
background-color: transparent
}
.btn-outline-primary:not(:disabled):not(.disabled).active,
.btn-outline-primary:not(:disabled):not(.disabled):active,
.show > .btn-outline-primary.dropdown-toggle {
color: #e9ecef;
background-color: #3395ff;
border-color: #3395ff
}
.btn-outline-primary:not(:disabled):not(.disabled).active:focus,
.btn-outline-primary:not(:disabled):not(.disabled):active:focus,
.show > .btn-outline-primary.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(51,149,255,.5)
}
.btn-outline-secondary {
color: #6c757d;
border-color: #6c757d
}
.btn-outline-secondary:hover {
color: #e9ecef;
background-color: #6c757d;
border-color: #6c757d
}
.btn-outline-secondary.focus,
.btn-outline-secondary:focus {
box-shadow: 0 0 0 .2rem rgba(108,117,125,.5)
}
.btn-outline-secondary.disabled,
.btn-outline-secondary:disabled {
color: #6c757d;
background-color: transparent
}
.btn-outline-secondary:not(:disabled):not(.disabled).active,
.btn-outline-secondary:not(:disabled):not(.disabled):active,
.show > .btn-outline-secondary.dropdown-toggle {
color: #e9ecef;
background-color: #6c757d;
border-color: #6c757d
}
.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,
.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,
.show > .btn-outline-secondary.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(108,117,125,.5)
}
.btn-outline-success {
color: #28a745;
border-color: #28a745
}
.btn-outline-success:hover {
color: #e9ecef;
background-color: #28a745;
border-color: #28a745
}
.btn-outline-success.focus,
.btn-outline-success:focus {
box-shadow: 0 0 0 .2rem rgba(40,167,69,.5)
}
.btn-outline-success.disabled,
.btn-outline-success:disabled {
color: #28a745;
background-color: transparent
}
.btn-outline-success:not(:disabled):not(.disabled).active,
.btn-outline-success:not(:disabled):not(.disabled):active,
.show > .btn-outline-success.dropdown-toggle {
color: #e9ecef;
background-color: #28a745;
border-color: #28a745
}
.btn-outline-success:not(:disabled):not(.disabled).active:focus,
.btn-outline-success:not(:disabled):not(.disabled):active:focus,
.show > .btn-outline-success.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(40,167,69,.5)
}
.btn-outline-info {
color: #17a2b8;
border-color: #17a2b8
}
.btn-outline-info:hover {
color: #e9ecef;
background-color: #17a2b8;
border-color: #17a2b8
}
.btn-outline-info.focus,
.btn-outline-info:focus {
box-shadow: 0 0 0 .2rem rgba(23,162,184,.5)
}
.btn-outline-info.disabled,
.btn-outline-info:disabled {
color: #17a2b8;
background-color: transparent
}
.btn-outline-info:not(:disabled):not(.disabled).active,
.btn-outline-info:not(:disabled):not(.disabled):active,
.show > .btn-outline-info.dropdown-toggle {
color: #e9ecef;
background-color: #17a2b8;
border-color: #17a2b8
}
.btn-outline-info:not(:disabled):not(.disabled).active:focus,
.btn-outline-info:not(:disabled):not(.disabled):active:focus,
.show > .btn-outline-info.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(23,162,184,.5)
}
.btn-outline-warning {
color: #fd7e14;
border-color: #fd7e14
}
.btn-outline-warning:hover {
color: #343a40;
background-color: #fd7e14;
border-color: #fd7e14
}
.btn-outline-warning.focus,
.btn-outline-warning:focus {
box-shadow: 0 0 0 .2rem rgba(253,126,20,.5)
}
.btn-outline-warning.disabled,
.btn-outline-warning:disabled {
color: #fd7e14;
background-color: transparent
}
.btn-outline-warning:not(:disabled):not(.disabled).active,
.btn-outline-warning:not(:disabled):not(.disabled):active,
.show > .btn-outline-warning.dropdown-toggle {
color: #343a40;
background-color: #fd7e14;
border-color: #fd7e14
}
.btn-outline-warning:not(:disabled):not(.disabled).active:focus,
.btn-outline-warning:not(:disabled):not(.disabled):active:focus,
.show > .btn-outline-warning.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(253,126,20,.5)
}
.btn-outline-danger {
color: #dc3545;
border-color: #dc3545
}
.btn-outline-danger:hover {
color: #e9ecef;
background-color: #dc3545;
border-color: #dc3545
}
.btn-outline-danger.focus,
.btn-outline-danger:focus {
box-shadow: 0 0 0 .2rem rgba(220,53,69,.5)
}
.btn-outline-danger.disabled,
.btn-outline-danger:disabled {
color: #dc3545;
background-color: transparent
}
.btn-outline-danger:not(:disabled):not(.disabled).active,
.btn-outline-danger:not(:disabled):not(.disabled):active,
.show > .btn-outline-danger.dropdown-toggle {
color: #e9ecef;
background-color: #dc3545;
border-color: #dc3545
}
.btn-outline-danger:not(:disabled):not(.disabled).active:focus,
.btn-outline-danger:not(:disabled):not(.disabled):active:focus,
.show > .btn-outline-danger.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(220,53,69,.5)
}
.btn-outline-light {
color: #dee2e6;
border-color: #dee2e6
}
.btn-outline-light:hover {
color: #343a40;
background-color: #dee2e6;
border-color: #dee2e6
}
.btn-outline-light.focus,
.btn-outline-light:focus {
box-shadow: 0 0 0 .2rem rgba(222,226,230,.5)
}
.btn-outline-light.disabled,
.btn-outline-light:disabled {
color: #dee2e6;
background-color: transparent
}
.btn-outline-light:not(:disabled):not(.disabled).active,
.btn-outline-light:not(:disabled):not(.disabled):active,
.show > .btn-outline-light.dropdown-toggle {
color: #343a40;
background-color: #dee2e6;
border-color: #dee2e6
}
.btn-outline-light:not(:disabled):not(.disabled).active:focus,
.btn-outline-light:not(:disabled):not(.disabled):active:focus,
.show > .btn-outline-light.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(222,226,230,.5)
}
.btn-outline-dark {
color: #343a40;
border-color: #343a40
}
.btn-outline-dark:hover {
color: #e9ecef;
background-color: #343a40;
border-color: #343a40
}
.btn-outline-dark.focus,
.btn-outline-dark:focus {
box-shadow: 0 0 0 .2rem rgba(52,58,64,.5)
}
.btn-outline-dark.disabled,
.btn-outline-dark:disabled {
color: #343a40;
background-color: transparent
}
.btn-outline-dark:not(:disabled):not(.disabled).active,
.btn-outline-dark:not(:disabled):not(.disabled):active,
.show > .btn-outline-dark.dropdown-toggle {
color: #e9ecef;
background-color: #343a40;
border-color: #343a40
}
.btn-outline-dark:not(:disabled):not(.disabled).active:focus,
.btn-outline-dark:not(:disabled):not(.disabled):active:focus,
.show > .btn-outline-dark.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem rgba(52,58,64,.5)
}
.btn-link {
color: #adadad
}
.btn-link:hover {
color: #878787
}
.btn-link.disabled,
.btn-link:disabled {
color: #6c757d
}
}
@media (prefers-color-scheme:dark) {
.dropdown-toggle::after {
border-top: .3em solid;
border-right: .3em solid transparent;
border-bottom: 0;
border-left: .3em solid transparent
}
.dropdown-menu {
color: #d3d3d3;
background-color: #000;
border: 1px solid rgba(255,255,255,.15)
}
}
@media (prefers-color-scheme:dark) {
.dropup .dropdown-toggle::after {
border-top: 0;
border-right: .3em solid transparent;
border-bottom: .3em solid;
border-left: .3em solid transparent
}
.dropright .dropdown-toggle::after {
border-top: .3em solid transparent;
border-right: 0;
border-bottom: .3em solid transparent;
border-left: .3em solid
}
.dropleft .dropdown-toggle::before {
border-top: .3em solid transparent;
border-right: .3em solid;
border-bottom: .3em solid transparent
}
.dropdown-divider {
border-top: 1px solid #343a40
}
.dropdown-item {
color: #f8f9fa;
background-color: transparent;
border: 0
}
.dropdown-item:focus,
.dropdown-item:hover {
color: #fff;
background-color: #212529
}
.dropdown-item.active,
.dropdown-item:active {
color: #000;
background-color: #3395ff
}
.dropdown-item.disabled,
.dropdown-item:disabled {
color: #ced4da;
background-color: transparent
}
.dropdown-header {
color: #ced4da
}
.dropdown-item-text {
color: #f8f9fa
}
.input-group-text {
color: #dee2e6;
background-color: #343a40;
border: 1px solid #6c757d
}
.custom-control-input:checked ~ .custom-control-label::before {
color: #fff;
border-color: #007bff;
background-color: #007bff
}
.custom-control-input:focus ~ .custom-control-label::before {
box-shadow: 0 0 0 .2rem rgba(0,123,255,.25)
}
.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
border-color: #80bdff
}
.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
color: #fff;
background-color: #b3d7ff;
border-color: #b3d7ff
}
.custom-control-input:disabled ~ .custom-control-label,
.custom-control-input[disabled] ~ .custom-control-label {
color: #6c757d
}
.custom-control-input:disabled ~ .custom-control-label::before,
.custom-control-input[disabled] ~ .custom-control-label::before {
background-color: #e9ecef
}
.custom-control-label::before {
background-color: #fff;
border: 1px solid #adb5bd
}
.custom-control-label::after {
background: 50%/50% 50% no-repeat
}
.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")
}
.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {
border-color: #007bff;
background-color: #007bff
}
.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")
}
.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {
background-color: rgba(0,123,255,.5)
}
.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {
background-color: rgba(0,123,255,.5)
}
.custom-radio .custom-control-input:checked ~ .custom-control-label::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")
}
.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {
background-color: rgba(0,123,255,.5)
}
.custom-switch .custom-control-label::after {
background-color: #adb5bd
}
}
@media (prefers-color-scheme:dark) {
.custom-switch .custom-control-input:checked ~ .custom-control-label::after {
background-color: #fff
}
.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {
background-color: rgba(0,123,255,.5)
}
.custom-select {
color: #dee2e6;
background: #000 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat;
border: 1px solid #6c757d
}
.custom-select:focus {
border-color: #80bdff;
box-shadow: 0 0 0 .2rem rgba(0,123,255,.25)
}
.custom-select:focus::-ms-value {
color: #dee2e6;
background-color: #000
}
.custom-select[multiple],
.custom-select[size]:not([size="1"]) {
background-image: none
}
.custom-select:disabled {
color: #ced4da;
background-color: #343a40
}
.custom-select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 #dee2e6
}
.custom-file-input:focus ~ .custom-file-label {
border-color: #80bdff;
box-shadow: 0 0 0 .2rem rgba(0,123,255,.25)
}
.custom-file-input:disabled ~ .custom-file-label,
.custom-file-input[disabled] ~ .custom-file-label {
background-color: #e9ecef
}
.custom-file-label {
color: #495057;
background-color: #fff;
border: 1px solid #ced4da
}
.custom-file-label::after {
color: #495057;
background-color: #e9ecef;
border-left: inherit
}
.custom-range {
background-color: transparent
}
.custom-range:focus::-webkit-slider-thumb {
box-shadow: 0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)
}
.custom-range:focus::-moz-range-thumb {
box-shadow: 0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)
}
.custom-range:focus::-ms-thumb {
box-shadow: 0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)
}
.custom-range::-moz-focus-outer {
border: 0
}
.custom-range::-webkit-slider-thumb {
background-color: #007bff;
border: 0
}
}
@media (prefers-color-scheme:dark) {
.custom-range::-webkit-slider-thumb:active {
background-color: #b3d7ff
}
.custom-range::-webkit-slider-runnable-track {
color: transparent;
background-color: #dee2e6;
border-color: transparent
}
.custom-range::-moz-range-thumb {
background-color: #007bff;
border: 0
}
}
@media (prefers-color-scheme:dark) {
.custom-range::-moz-range-thumb:active {
background-color: #b3d7ff
}
.custom-range::-moz-range-track {
color: transparent;
background-color: #dee2e6;
border-color: transparent
}
.custom-range::-ms-thumb {
background-color: #007bff;
border: 0
}
}
@media (prefers-color-scheme:dark) {
.custom-range::-ms-thumb:active {
background-color: #b3d7ff
}
.custom-range::-ms-track {
color: transparent;
background-color: transparent;
border-color: transparent
}
.custom-range::-ms-fill-lower {
background-color: #dee2e6
}
.custom-range::-ms-fill-upper {
background-color: #dee2e6
}
.custom-range:disabled::-webkit-slider-thumb {
background-color: #adb5bd
}
.custom-range:disabled::-moz-range-thumb {
background-color: #adb5bd
}
.custom-range:disabled::-ms-thumb {
background-color: #adb5bd
}
}
@media (prefers-color-scheme:dark) {
.nav-link.disabled {
color: #6c757d
}
.nav-tabs {
border-bottom: 1px solid rgba(255,255,255,.125)
}
.nav-tabs .nav-link {
background-color: transparent;
border: 1px solid transparent
}
.nav-tabs .nav-link:focus,
.nav-tabs .nav-link:hover {
border-color: #495057 #495057 rgba(255,255,255,.125)
}
.nav-tabs .nav-link.disabled {
color: #6c757d;
background-color: transparent;
border-color: transparent
}
.nav-tabs .nav-item.show .nav-link,
.nav-tabs .nav-link.active {
color: #f8f9fa;
background-color: #191d21;
border-color: #495057 #495057 #191d21
}
.nav-pills .nav-link {
background: 0 0;
border: 0
}
.nav-pills .nav-link.active,
.nav-pills .show > .nav-link {
color: #fff;
background-color: #007bff
}
.navbar-toggler {
background-color: transparent;
border: 1px solid transparent
}
.navbar-toggler-icon {
background: 50%/100% 100% no-repeat
}
}
@media (prefers-color-scheme:dark) {
.navbar-light .navbar-brand {
color: rgba(0,0,0,.9)
}
.navbar-light .navbar-brand:focus,
.navbar-light .navbar-brand:hover {
color: rgba(0,0,0,.9)
}
.navbar-light .navbar-nav .nav-link {
color: rgba(0,0,0,.5)
}
.navbar-light .navbar-nav .nav-link:focus,
.navbar-light .navbar-nav .nav-link:hover {
color: rgba(0,0,0,.7)
}
.navbar-light .navbar-nav .nav-link.disabled {
color: rgba(0,0,0,.3)
}
.navbar-light .navbar-nav .active > .nav-link,
.navbar-light .navbar-nav .nav-link.active,
.navbar-light .navbar-nav .nav-link.show,
.navbar-light .navbar-nav .show > .nav-link {
color: rgba(0,0,0,.9)
}
.navbar-light .navbar-toggler {
color: rgba(0,0,0,.5);
border-color: rgba(0,0,0,.1)
}
.navbar-light .navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")
}
.navbar-light .navbar-text {
color: rgba(0,0,0,.5)
}
.navbar-light .navbar-text a {
color: rgba(0,0,0,.9)
}
.navbar-light .navbar-text a:focus,
.navbar-light .navbar-text a:hover {
color: rgba(0,0,0,.9)
}
.navbar-dark .navbar-brand,
.navbar-themed .navbar-brand {
color: #fff
}
.navbar-dark .navbar-brand:focus,
.navbar-dark .navbar-brand:hover,
.navbar-themed .navbar-brand:focus,
.navbar-themed .navbar-brand:hover {
color: #fff
}
.navbar-dark .navbar-nav .nav-link,
.navbar-themed .navbar-nav .nav-link {
color: rgba(255,255,255,.5)
}
.navbar-dark .navbar-nav .nav-link:focus,
.navbar-dark .navbar-nav .nav-link:hover,
.navbar-themed .navbar-nav .nav-link:focus,
.navbar-themed .navbar-nav .nav-link:hover {
color: rgba(255,255,255,.75)
}
.navbar-dark .navbar-nav .nav-link.disabled,
.navbar-themed .navbar-nav .nav-link.disabled {
color: rgba(255,255,255,.25)
}
.navbar-dark .navbar-nav .active > .nav-link,
.navbar-dark .navbar-nav .nav-link.active,
.navbar-dark .navbar-nav .nav-link.show,
.navbar-dark .navbar-nav .show > .nav-link,
.navbar-themed .navbar-nav .active > .nav-link,
.navbar-themed .navbar-nav .nav-link.active,
.navbar-themed .navbar-nav .nav-link.show,
.navbar-themed .navbar-nav .show > .nav-link {
color: #fff
}
.navbar-dark .navbar-toggler,
.navbar-themed .navbar-toggler {
color: rgba(255,255,255,.5);
border-color: rgba(255,255,255,.1)
}
.navbar-dark .navbar-toggler-icon,
.navbar-themed .navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")
}
.navbar-dark .navbar-text,
.navbar-themed .navbar-text {
color: rgba(255,255,255,.5)
}
.navbar-dark .navbar-text a,
.navbar-themed .navbar-text a {
color: #fff
}
.navbar-dark .navbar-text a:focus,
.navbar-dark .navbar-text a:hover,
.navbar-themed .navbar-text a:focus,
.navbar-themed .navbar-text a:hover {
color: #fff
}
.card {
background-color: #212529;
border: 1px solid rgba(255,255,255,.125)
}
.card > .list-group {
border-top: inherit;
border-bottom: inherit
}
.card > .card-header + .list-group,
.card > .list-group + .card-footer {
border-top: 0
}
.card-header {
background-color: rgba(255,255,255,.03);
border-bottom: 1px solid rgba(255,255,255,.125)
}
.card-footer {
background-color: rgba(255,255,255,.03);
border-top: 1px solid rgba(255,255,255,.125)
}
.card-header-tabs {
border-bottom: 0
}
}
@media (prefers-color-scheme:dark) and (min-width:576px) {
.card-group > .card + .card {
border-left: 0
}
}
@media (prefers-color-scheme:dark) {
.accordion > .card:not(:last-of-type) {
border-bottom: 0
}
.breadcrumb {
background-color: #343a40
}
.breadcrumb-item + .breadcrumb-item::before {
color: #ced4da
}
.breadcrumb-item.active {
color: #ced4da
}
.page-link {
color: #adadad;
background-color: #000;
border: 1px solid #495057
}
.page-link:hover {
color: #878787;
background-color: #343a40;
border-color: #495057
}
.page-link:focus {
box-shadow: 0 0 0 .2rem rgba(0,123,255,.25)
}
.page-item.active .page-link {
color: #000;
background-color: #3395ff;
border-color: #3395ff
}
.page-item.disabled .page-link {
color: #ced4da;
background-color: #000;
border-color: #495057
}
}
@media (prefers-color-scheme:dark) {
.badge-primary {
color: #e9ecef;
background-color: #3395ff
}
a.badge-primary:focus,
a.badge-primary:hover {
color: #e9ecef;
background-color: #007bff
}
a.badge-primary.focus,
a.badge-primary:focus {
box-shadow: 0 0 0 .2rem rgba(51,149,255,.5)
}
.badge-secondary {
color: #e9ecef;
background-color: #6c757d
}
a.badge-secondary:focus,
a.badge-secondary:hover {
color: #e9ecef;
background-color: #545b62
}
a.badge-secondary.focus,
a.badge-secondary:focus {
box-shadow: 0 0 0 .2rem rgba(108,117,125,.5)
}
.badge-success {
color: #e9ecef;
background-color: #28a745
}
a.badge-success:focus,
a.badge-success:hover {
color: #e9ecef;
background-color: #1e7e34
}
a.badge-success.focus,
a.badge-success:focus {
box-shadow: 0 0 0 .2rem rgba(40,167,69,.5)
}
.badge-info {
color: #e9ecef;
background-color: #17a2b8
}
a.badge-info:focus,
a.badge-info:hover {
color: #e9ecef;
background-color: #117a8b
}
a.badge-info.focus,
a.badge-info:focus {
box-shadow: 0 0 0 .2rem rgba(23,162,184,.5)
}
.badge-warning {
color: #343a40;
background-color: #fd7e14
}
a.badge-warning:focus,
a.badge-warning:hover {
color: #343a40;
background-color: #dc6502
}
a.badge-warning.focus,
a.badge-warning:focus {
box-shadow: 0 0 0 .2rem rgba(253,126,20,.5)
}
.badge-danger {
color: #e9ecef;
background-color: #dc3545
}
a.badge-danger:focus,
a.badge-danger:hover {
color: #e9ecef;
background-color: #bd2130
}
a.badge-danger.focus,
a.badge-danger:focus {
box-shadow: 0 0 0 .2rem rgba(220,53,69,.5)
}
.badge-light {
color: #343a40;
background-color: #dee2e6
}
a.badge-light:focus,
a.badge-light:hover {
color: #343a40;
background-color: #c1c9d0
}
a.badge-light.focus,
a.badge-light:focus {
box-shadow: 0 0 0 .2rem rgba(222,226,230,.5)
}
.badge-dark {
color: #e9ecef;
background-color: #343a40
}
a.badge-dark:focus,
a.badge-dark:hover {
color: #e9ecef;
background-color: #1d2124
}
a.badge-dark.focus,
a.badge-dark:focus {
box-shadow: 0 0 0 .2rem rgba(52,58,64,.5)
}
.jumbotron {
background-color: #343a40
}
}
@media (prefers-color-scheme:dark) {
.alert {
border: 1px solid transparent
}
.alert-heading {
color: inherit
}
.alert-dismissible .close {
color: inherit
}
.alert-primary {
color: #1b4e85;
background-color: #d6eaff;
border-color: #c6e1ff
}
.alert-primary .alert-link {
color: #12355b
}
.alert-secondary {
color: #383d41;
background-color: #e2e3e5;
border-color: #d6d8db
}
.alert-secondary .alert-link {
color: #202326
}
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb
}
.alert-success .alert-link {
color: #0b2e13
}
.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb
}
.alert-info .alert-link {
color: #062c33
}
.alert-warning {
color: #84420a;
background-color: #ffe5d0;
border-color: #fedbbd
}
.alert-warning .alert-link {
color: #552a06
}
.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb
}
.alert-danger .alert-link {
color: #491217
}
.alert-light,
.alert-themed-inverted {
color: #737678;
background-color: #f8f9fa;
border-color: #f6f7f8
}
.alert-light .alert-link,
.alert-themed-inverted .alert-link {
color: #5a5c5e
}
.alert-dark,
.alert-themed {
color: #1b1e21;
background-color: #d6d8d9;
border-color: #c6c8ca
}
.alert-dark .alert-link,
.alert-themed .alert-link {
color: #040505
}
@-webkit-keyframes progress-bar-stripes {
from {
background-position: 1rem 0
}
to {
background-position: 0 0
}
}
@keyframes progress-bar-stripes {
from {
background-position: 1rem 0
}
to {
background-position: 0 0
}
}
.progress {
background-color: #e9ecef
}
.progress-bar {
color: #fff;
background-color: #007bff
}
}
@media (prefers-color-scheme:dark) {
.progress-bar-striped {
background-image: linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)
}
}
@media (prefers-color-scheme:dark) {
.list-group-item-action {
color: #dee2e6
}
.list-group-item-action:focus,
.list-group-item-action:hover {
color: #dee2e6;
background-color: #212529
}
.list-group-item-action:active {
color: #d3d3d3;
background-color: #343a40
}
.list-group-item {
background-color: rgba(25,29,33,.05);
border: 1px solid rgba(255,255,255,.125)
}
.list-group-item.disabled,
.list-group-item:disabled {
color: #ced4da;
background-color: rgba(25,29,33,.05)
}
.list-group-item.active {
color: #000;
background-color: #3395ff;
border-color: #3395ff
}
}
@media (prefers-color-scheme:dark) {
.list-group-item-primary {
color: #1b4e85;
background-color: #c6e1ff
}
.list-group-item-primary.list-group-item-action:focus,
.list-group-item-primary.list-group-item-action:hover {
color: #1b4e85;
background-color: #add4ff
}
.list-group-item-primary.list-group-item-action.active {
color: #fff;
background-color: #1b4e85;
border-color: #1b4e85
}
.list-group-item-secondary {
color: #383d41;
background-color: #d6d8db
}
.list-group-item-secondary.list-group-item-action:focus,
.list-group-item-secondary.list-group-item-action:hover {
color: #383d41;
background-color: #c8cbcf
}
.list-group-item-secondary.list-group-item-action.active {
color: #fff;
background-color: #383d41;
border-color: #383d41
}
.list-group-item-success {
color: #155724;
background-color: #c3e6cb
}
.list-group-item-success.list-group-item-action:focus,
.list-group-item-success.list-group-item-action:hover {
color: #155724;
background-color: #b1dfbb
}
.list-group-item-success.list-group-item-action.active {
color: #fff;
background-color: #155724;
border-color: #155724
}
.list-group-item-info {
color: #0c5460;
background-color: #bee5eb
}
.list-group-item-info.list-group-item-action:focus,
.list-group-item-info.list-group-item-action:hover {
color: #0c5460;
background-color: #abdde5
}
.list-group-item-info.list-group-item-action.active {
color: #fff;
background-color: #0c5460;
border-color: #0c5460
}
.list-group-item-warning {
color: #84420a;
background-color: #fedbbd
}
.list-group-item-warning.list-group-item-action:focus,
.list-group-item-warning.list-group-item-action:hover {
color: #84420a;
background-color: #fecda4
}
.list-group-item-warning.list-group-item-action.active {
color: #fff;
background-color: #84420a;
border-color: #84420a
}
.list-group-item-danger {
color: #721c24;
background-color: #f5c6cb
}
.list-group-item-danger.list-group-item-action:focus,
.list-group-item-danger.list-group-item-action:hover {
color: #721c24;
background-color: #f1b0b7
}
.list-group-item-danger.list-group-item-action.active {
color: #fff;
background-color: #721c24;
border-color: #721c24
}
.list-group-item-light {
color: #737678;
background-color: #f6f7f8
}
.list-group-item-light.list-group-item-action:focus,
.list-group-item-light.list-group-item-action:hover {
color: #737678;
background-color: #e8eaed
}
.list-group-item-light.list-group-item-action.active {
color: #fff;
background-color: #737678;
border-color: #737678
}
.list-group-item-dark {
color: #1b1e21;
background-color: #c6c8ca
}
.list-group-item-dark.list-group-item-action:focus,
.list-group-item-dark.list-group-item-action:hover {
color: #1b1e21;
background-color: #b9bbbe
}
.list-group-item-dark.list-group-item-action.active {
color: #fff;
background-color: #1b1e21;
border-color: #1b1e21
}
.close {
color: #fff;
text-shadow: 0 1px 0 #000
}
.close:hover {
color: #fff
}
button.close {
background-color: transparent;
border: 0
}
.toast {
background-color: rgba(0,0,0,.85);
border: 1px solid rgba(255,255,255,.1);
box-shadow: 0 .25rem .75rem rgba(255,255,255,.1)
}
.toast-header {
color: #ced4da;
background-color: rgba(0,0,0,.85);
border-bottom: 1px solid rgba(255,255,255,.05)
}
}
@media (prefers-color-scheme:dark) {
.modal-content {
background-color: #191d21;
border: 1px solid rgba(255,255,255,.2)
}
.modal-backdrop {
background-color: #000
}
.modal-header {
border-bottom: 1px solid #343a40
}
.modal-footer {
border-top: 1px solid #343a40
}
}
@media (prefers-color-scheme:dark) {
.tooltip {
text-shadow: none
}
.tooltip .arrow::before {
border-color: transparent
}
.tooltip-inner {
color: #fff;
background-color: #000
}
.popover {
text-shadow: none;
background-color: #fff;
border: 1px solid rgba(0,0,0,.2)
}
.popover .arrow::after,
.popover .arrow::before {
border-color: transparent
}
.bs-popover-auto[x-placement^=bottom] .popover-header::before,
.bs-popover-bottom .popover-header::before {
border-bottom: 1px solid #f7f7f7
}
.popover-header {
background-color: #f7f7f7;
border-bottom: 1px solid #ebebeb
}
.popover-body {
color: #212529
}
}
@media (prefers-color-scheme:dark) {
.carousel-control-next,
.carousel-control-prev {
color: #fff;
background: 0 0;
border: 0
}
}
@media (prefers-color-scheme:dark) {
.carousel-control-next:focus,
.carousel-control-next:hover,
.carousel-control-prev:focus,
.carousel-control-prev:hover {
color: #fff
}
.carousel-control-next-icon,
.carousel-control-prev-icon {
background: 50%/100% 100% no-repeat
}
.carousel-control-prev-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")
}
.carousel-control-next-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")
}
.carousel-indicators li {
background-color: #fff;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent
}
}
@media (prefers-color-scheme:dark) {
.carousel-caption {
color: #fff
}
@-webkit-keyframes spinner-border {
to {
transform: rotate(360deg)
}
}
@keyframes spinner-border {
to {
transform: rotate(360deg)
}
}
.spinner-border {
border: .25em solid currentcolor
}
@-webkit-keyframes spinner-grow {
0% {
transform: scale(0)
}
50% {
opacity: 1;
transform: none
}
}
@keyframes spinner-grow {
0% {
transform: scale(0)
}
50% {
opacity: 1;
transform: none
}
}
.spinner-grow {
background-color: currentcolor
}
}
@media (prefers-color-scheme:dark) {
.bg-primary {
background-color: #3395ff!important
}
a.bg-primary:focus,
a.bg-primary:hover,
button.bg-primary:focus,
button.bg-primary:hover {
background-color: #007bff!important
}
.bg-secondary {
background-color: #6c757d!important
}
a.bg-secondary:focus,
a.bg-secondary:hover,
button.bg-secondary:focus,
button.bg-secondary:hover {
background-color: #545b62!important
}
.bg-success {
background-color: #28a745!important
}
a.bg-success:focus,
a.bg-success:hover,
button.bg-success:focus,
button.bg-success:hover {
background-color: #1e7e34!important
}
.bg-info {
background-color: #17a2b8!important
}
a.bg-info:focus,
a.bg-info:hover,
button.bg-info:focus,
button.bg-info:hover {
background-color: #117a8b!important
}
.bg-warning {
background-color: #fd7e14!important
}
a.bg-warning:focus,
a.bg-warning:hover,
button.bg-warning:focus,
button.bg-warning:hover {
background-color: #dc6502!important
}
.bg-danger {
background-color: #dc3545!important
}
a.bg-danger:focus,
a.bg-danger:hover,
button.bg-danger:focus,
button.bg-danger:hover {
background-color: #bd2130!important
}
.bg-light,
.bg-themed-inverted {
background-color: #dee2e6!important
}
a.bg-light:focus,
a.bg-light:hover,
a.bg-themed-inverted:focus,
a.bg-themed-inverted:hover,
button.bg-light:focus,
button.bg-light:hover,
button.bg-themed-inverted:focus,
button.bg-themed-inverted:hover {
background-color: #c1c9d0!important
}
.bg-dark,
.bg-themed,
.navbar-themed {
background-color: #343a40!important
}
a.bg-dark:focus,
a.bg-dark:hover,
a.bg-themed:focus,
a.bg-themed:hover,
a.navbar-themed:focus,
a.navbar-themed:hover,
button.bg-dark:focus,
button.bg-dark:hover,
button.bg-themed:focus,
button.bg-themed:hover,
button.navbar-themed:focus,
button.navbar-themed:hover {
background-color: #1d2124!important
}
.bg-white {
background-color: #fff!important
}
.bg-transparent {
background-color: transparent!important
}
.border {
border: 1px solid #343a40!important
}
.border-top {
border-top: 1px solid #343a40!important
}
.border-right {
border-right: 1px solid #343a40!important
}
.border-bottom {
border-bottom: 1px solid #343a40!important
}
.border-left {
border-left: 1px solid #343a40!important
}
.border-0 {
border: 0!important
}
.border-top-0 {
border-top: 0!important
}
.border-right-0 {
border-right: 0!important
}
.border-bottom-0 {
border-bottom: 0!important
}
.border-left-0 {
border-left: 0!important
}
.border-primary {
border-color: #3395ff!important
}
.border-secondary {
border-color: #6c757d!important
}
.border-success {
border-color: #28a745!important
}
.border-info {
border-color: #17a2b8!important
}
.border-warning {
border-color: #fd7e14!important
}
.border-danger {
border-color: #dc3545!important
}
.border-light {
border-color: #dee2e6!important
}
.border-dark {
border-color: #343a40!important
}
.border-white {
border-color: #fff!important
}
}
@media (prefers-color-scheme:dark) {
.embed-responsive .embed-responsive-item,
.embed-responsive embed,
.embed-responsive iframe,
.embed-responsive object,
.embed-responsive video {
border: 0
}
}
@media (prefers-color-scheme:dark) {
@supports ((position:-webkit-sticky) or (position:sticky)) {
.sticky-top {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 1020
}
}
.sr-only {
border: 0
}
.shadow-sm {
box-shadow: 0 .125rem .25rem rgba(0,0,0,.075)!important
}
.shadow {
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important
}
.shadow-lg {
box-shadow: 0 1rem 3rem rgba(0,0,0,.175)!important
}
.shadow-none {
box-shadow: none!important
}
}
@media (prefers-color-scheme:dark) {
.stretched-link::after {
background-color: rgba(0,0,0,0)
}
}
@media (prefers-color-scheme:dark) {
.text-white {
color: #fff!important
}
.text-primary {
color: #3395ff!important
}
a.text-primary:focus,
a.text-primary:hover {
color: #006fe6!important
}
.text-secondary {
color: #6c757d!important
}
a.text-secondary:focus,
a.text-secondary:hover {
color: #494f54!important
}
.text-success {
color: #28a745!important
}
a.text-success:focus,
a.text-success:hover {
color: #19692c!important
}
.text-info {
color: #17a2b8!important
}
a.text-info:focus,
a.text-info:hover {
color: #0f6674!important
}
.text-warning {
color: #fd7e14!important
}
a.text-warning:focus,
a.text-warning:hover {
color: #c35a02!important
}
.text-danger {
color: #dc3545!important
}
a.text-danger:focus,
a.text-danger:hover {
color: #a71d2a!important
}
.text-light,
.text-themed {
color: #dee2e6!important
}
a.text-light:focus,
a.text-light:hover,
a.text-themed:focus,
a.text-themed:hover {
color: #b2bcc5!important
}
.text-dark,
.text-themed-inverted {
color: #343a40!important
}
a.text-dark:focus,
a.text-dark:hover,
a.text-themed-inverted:focus,
a.text-themed-inverted:hover {
color: #121416!important
}
.text-body {
color: #d3d3d3!important
}
.text-muted {
color: #6c757d!important
}
.text-black-50 {
color: rgba(0,0,0,.5)!important
}
.text-white-50 {
color: rgba(255,255,255,.5)!important
}
.text-hide {
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0
}
.text-reset {
color: inherit!important
}
}
\ No newline at end of file