Initial mfa support (for internal users) #93591

Merged
Oleg-Komarov merged 46 commits from mfa into main 2024-08-29 11:44:06 +02:00
26 changed files with 289 additions and 177 deletions
Showing only changes of commit e442378a03 - Show all commits

1
.gitmodules vendored
View File

@ -3,6 +3,7 @@
# Please keep this a publicly accessible URL, as it is used for the
# automatic deployment.
url = https://projects.blender.org/infrastructure/web-assets.git
branch = v2
[submodule "playbooks/shared"]
path = playbooks/shared
url = https://projects.blender.org/infrastructure/web-playbooks

@ -1 +1 @@
Subproject commit 41b3c1678602b71a18f0e29f3abc59a03b878beb
Subproject commit d37350f747f54c0a011904ad6572af8ce85adb0e

View File

@ -1,5 +1,5 @@
body.is-auth-page
--btn-accent-box-shadow: 0px 4px 10px rgba(0,82,255,.2), 1px 10px 25px rgba(0,82,255,.1)
--btn-accent-box-shadow: 0px var(--spacer-1) var(--spacer-2) rgba(0,82,255,.2), .1rem var(--spacer-2) var(--spacer-4) rgba(0,82,255,.1)
.container-main
align-items: center
@ -8,26 +8,31 @@ body.is-auth-page
height: 100%
justify-content: center
.container
max-height: 100%
overflow-y: auto
.box
backdrop-filter: blur(25px)
background-color: rgba(white, .66)
box-shadow: 0 15px 50px rgba(black, .15)
margin-bottom: 6rem
padding: 2rem
@extend .bg-filter-blur
@extend .bg-noise
box-shadow: 0 var(--spacer) var(--spacer-5) rgba(black, .15)
margin-bottom: var(--spacer-1)
padding: calc(var(--spacer) * 2)
.btn-accent
padding-block: .33rem
padding-block: .2rem
input.form-control
border-color: transparent
height: unset
&:focus
border-color: var(--color-primary)
border-color: var(--color-accent)
.blender-logo
filter: drop-shadow(2px 0px 15px rgba(0,0,0,0.15))
margin: 1rem auto
filter: drop-shadow(.2rem 0 var(--spacer) rgba(0,0,0,0.15))
margin: calc(var(--spacer) * 2) auto
z-index: 1
form
@ -40,15 +45,15 @@ body.is-auth-page
right: 0
z-index: 1
.page-background
.page-bg
background-size: cover
bottom: 0
height: 100%
left: 0
opacity: .8
pointer-events: none
position: fixed
right: 0
position: absolute
top: 0
width: 100%
&::after
background-color: transparent
@ -60,12 +65,15 @@ body.is-auth-page
.blender-logo
display: block
width: 250px
svg
height: calc(var(--spacer) * 5)
max-width: 100%
.auth-notes
background-color: rgba(black, .05)
border-bottom-right-radius: var(--border-radius)
border-left: 4px solid rgba(black, .2)
border-left: var(--spacer-1) solid rgba(black, .2)
border-top-right-radius: var(--border-radius)
+margin(3, top)
+padding(3, left)

View File

@ -1,49 +1,13 @@
@import toastr.min
// Import variables.
@import '../../../../assets_shared/src/styles/_media_queries'
@import '../../../../assets_shared/src/styles/_variables'
/* Import Bootstrap. */
@import '../../../../assets_shared/src/styles/bootstrap/_functions'
@import '../../../../assets_shared/src/styles/bootstrap/_variables'
@import '../../../../assets_shared/src/styles/bootstrap/_mixins'
@import '../../../../assets_shared/src/styles/bootstrap/_root'
@import '../../../../assets_shared/src/styles/bootstrap/_reboot'
@import '../../../../assets_shared/src/styles/bootstrap/_images'
@import '../../../../assets_shared/src/styles/bootstrap/_grid'
@import '../../../../assets_shared/src/styles/bootstrap/_forms'
@import '../../../../assets_shared/src/styles/bootstrap/_transitions'
@import '../../../../assets_shared/src/styles/bootstrap/_nav'
@import '../../../../assets_shared/src/styles/bootstrap/_navbar'
@import '../../../../assets_shared/src/styles/bootstrap/_media'
@import '../../../../assets_shared/src/styles/bootstrap/_utilities'
@import '../../../../assets_shared/src/styles/_mixins'
@import '../../../../assets_shared/src/styles/_utilities'
$font-path: '/static/assets/fonts'
@import '../../../../assets_shared/src/styles/_fonts'
@import '../../../../assets_shared/src/styles/_bootstrap_overrides'
@import '../../../../assets_shared/src/styles/_alert'
@import '../../../../assets_shared/src/styles/_base'
@import '../../../../assets_shared/src/styles/_box'
@import '../../../../assets_shared/src/styles/_button'
@import '../../../../assets_shared/src/styles/_cards'
@import '../../../../assets_shared/src/styles/_code'
@import '../../../../assets_shared/src/styles/_footer'
@import '../../../../assets_shared/src/styles/_forms'
@import '../../../../assets_shared/src/styles/_hero'
@import '../../../../assets_shared/src/styles/_list'
@import '../../../../assets_shared/src/styles/_navigation'
@import '../../../../assets_shared/src/styles/_navigation_global'
@import '../../../../assets_shared/src/styles/_sidebar'
@import '../../../../assets_shared/src/styles/_type'
/* Import Blender Web Assets. */
@import '../../../../assets_shared/src/styles/main'
@import '_auth'
// TODO: check explicit font-sizes
.bid
&-user // .bid-user
align-items: center
@ -58,7 +22,8 @@ $font-path: '/static/assets/fonts'
&-links // .bid-links
display: flex
font-size: var(--font-size-small)
font-size: var(--fs-sm)
line-height: var(--lh-sm)
justify-content: space-between
+list-unstyled
margin: var(--spacer) 0 0 0
@ -75,7 +40,7 @@ $font-path: '/static/assets/fonts'
&-header // .bid-header
font-size: 1.1em
margin: 10px
margin: var(--spacer-2)
&-tokens // .bid-tokens
+list-unstyled
@ -94,13 +59,13 @@ $font-path: '/static/assets/fonts'
.app-url
color: var(--text-color-secondary)
font-size: var(--font-size-small)
font-size: var(--fs-sm)
.app-icon
border-radius: var(--border-radius)
height: 48px
height: var(--spacer-5)
+margin(3, right)
width: 48px
width: var(--spacer-5)
.app
&-url
@ -113,6 +78,13 @@ $font-path: '/static/assets/fonts'
&:hover
color: lighten(var(--color-danger), 10%)
&.box
overflow-x: auto
td,
th
white-space: nowrap
.app-info
display: flex
flex: 1
@ -125,7 +97,7 @@ $font-path: '/static/assets/fonts'
position: absolute
.cke
margin-top: 40px !important
margin-top: var(--spacer-5) !important
.authorize
padding: 20px
@ -135,32 +107,32 @@ $font-path: '/static/assets/fonts'
margin: 0 auto 15px
position: relative
img, .authorize__app-image_placeholder
img, .authorize__app-img_placeholder
height: 100px
width: 100px
.authorize__app-image
.authorize__app-img
background: #7596d9
margin-right: 20px
margin-right: var(--spacer-4)
.authorize__app-image_placeholder
.authorize__app-img_placeholder
background: #7596d9
margin-right: 20px
margin-right: var(--spacer-4)
.authorize__app-image_placeholder_icon
.authorize__app-img_placeholder_icon
position: absolute
top: 20px
left: 33px
top: var(--spacer-4)
left: calc(var(--spacer) * 2)
color: white
.authorize__person-image
.authorize__person-img
background: #afd876
.authorize__pictogram__arrow
margin-top: -98px
margin-top: -98px // TODO: check magic number
font-family: FontAwesome
font-size: 70px
font-size: 7.2rem
color: white
&:before
@ -170,42 +142,42 @@ $font-path: '/static/assets/fonts'
&__title
margin-bottom: 10px !important
margin-bottom: var(--spacer-2) !important
text-align: center
&__blurb
max-width: 60%
margin: 20px auto !important
font-size: 16px
margin: var(--spacer-4) auto !important
font-size: 1.6rem
text-shadow: none
text-align: center
&__info
font-size: 13px
font-size: var(-fs-sm)
color: #737373
&__review
padding: 20px 0
margin-bottom: 20px
padding: var(--spacer-4) 0
margin-bottom: var(--spacer-4)
border-top: 1px solid #dfdfdf
border-bottom: 1px solid #dfdfdf
border-top: .1rem solid #dfdfdf
border-bottom: .1rem solid #dfdfdf
h3
margin: 0 0 10px 15px
margin: 0 0 var(--spacer-2) var(--spacer)
ul
padding-left: 15px
padding-left: var(--spacer)
li
margin-bottom: 5px
margin-bottom: var(--spacer-1)
.btn-chooser
padding-bottom: 20px
padding-bottom: var(--spacer-4)
.btn
margin-right: 10px
margin-right: var(--spacer-2)
&:last-child
margin-right: 0
@ -215,11 +187,11 @@ $font-path: '/static/assets/fonts'
.box__learn-more
display: block
font-size: 12px
font-size: 1.2rem
text-align: center
.profile.address .box
padding: 15px
padding: var(--spacer)
.profile.address .box h2
padding: 0
@ -248,7 +220,7 @@ ul.errorlist
@media (min-width: 991px)
.box.decent-height
min-height: 400px
min-height: calc(var(--spacer) * 25)
.developer-apps
form .form-check-input
@ -259,15 +231,15 @@ ul.errorlist
h2 i
color: var(--color-warning)
.badge-image
--badge-image-size: 64px
.badge-img
--badge-img-size: calc(var(--spacer) * 4)
margin: 0
transition: opacity var(--transition-speed-slow)
img
border-radius: var(--border-radius-lg)
height: var(--badge-image-size)
width: var(--badge-image-size)
height: var(--badge-img-size)
width: var(--badge-img-size)
.badge-info
flex: 1
@ -286,21 +258,25 @@ ul.badges
+list-unstyled
margin: 0
.badge-image
.badge-img
+margin(3, right)
li
display: flex
align-items: center
+margin(3, bottom)
+padding(2, y)
&:last-child
margin-bottom: 0
/* Style private badges. */
&.is-private
.badge-info
color: var(--text-color-secondary)
opacity: .5
.badge-image
.badge-img
opacity: .3
.toggle-group
@ -320,32 +296,32 @@ ul.badges
width: 150%
&.off
background-color: var(--text-color-tertiary)
background-color: var(--color-text-tertiary)
.toggle-handle
border-color: var(--text-color-tertiary)
border-color: var(--color-text-tertiary)
.toggle-group
left: -50%
.toggle
background-color: var(--color-primary)
background-color: var(--color-accent)
border-radius: 999em
padding: 0
&:hover
background-color: var(--color-primary)
background-color: var(--color-accent)
&.btn
min-height: 30px !important
min-height: calc(var(--spacer) * 2) !important
.toggle .toggle-handle
background-color: white
border: none
border-radius: 999em
height: 20px
height: var(--spacer-4)
padding: 0
width: 20px
width: var(--spacer-4)
.badge-privacy-toggle
align-self: flex-start
@ -367,9 +343,9 @@ ul.badges
form
.profile-avatar
img
height: 150px
height: calc(var(--spacer) * 10)
max-width: initial
width: 150px
width: calc(var(--spacer) * 10)
.nav-global
.profile-avatar
@ -388,17 +364,111 @@ form
/* Larger icons */
i.icon.icon-lg
svg
width: 32px
height: 32px
max-width: 32px
max-height: 32px
width: calc(var(--spacer) * 2)
height: calc(var(--spacer) * 2)
max-width: calc(var(--spacer) * 2)
max-height: calc(var(--spacer) * 2)
/* Bootstrap's outline icons */
svg.bi
stroke: none
margin-top: -3px
margin-top: -.3rem // TODO: check magic number
.oauth-logo
max-width: 210px
max-height: 85px
// TODO: check magic numbers
max-width: 21.0rem
max-height: 8.5rem
width: 100%
/* Web Assets overrides. */
// TODO: @web-assets v2 check --input-color-bg-hover
\:root
--input-color-bg: white
--input-color-bg-hover: var(--input-color-bg)
.btn
justify-content: center
.footer-pages
line-height: var(--lh-sm)
a
+padding(0, y)
+padding(2, x)
form
// TODO: @web-assets check form buttons sizing height
.btn
align-items: center
display: flex
min-height: var(--spacer-5)
.form-check
align-items: center
display: flex
padding-left: 0
// TODO: @web-assets consolidate input checkbox styles
input
&[type="checkbox"]
height: auto
+margin(0, left)
+margin(2, right)
+margin(0, top)
position: static
transform: scale(1.25)
transform-origin: left
&:hover
cursor: pointer
.form-group
display: block
.form-text
ul
+margin(2, top)
+padding(3, left)
// TODO: @web-assets move input checkbox styles
input
&[type="checkbox"]
height: auto
+margin(2, right)
transform: scale(1.25)
transform-origin: left
&:hover
cursor: pointer
+media-lg
.nav-global-container
max-width: 117.0rem
/* Utilities. */
// TODO: make bg filter blur uniform in project
// TODO: @web-assets move to web-assets
.bg-filter-blur
background-color: transparent
backdrop-filter: blur(var(--spacer-4))
-webkit-backdrop-filter: blur(var(--spacer-4))
position: relative
&::before
background-color: var(--box-bg-color)
border-radius: var(--border-radius)
content: ""
height: 100%
left: 0
opacity: .8
position: absolute
top: 0
width: 100%
z-index: -1
.bg-noise
// SVG generated at https://fffuel.co/nnnoise
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns:svgjs='http://svgjs.dev/svgjs' viewBox='0 0 700 700' width='700' height='700'%3E%3Cdefs%3E%3Cfilter id='nnnoise-filter' x='-20%25' y='-20%25' width='140%25' height='140%25' filterUnits='objectBoundingBox' primitiveUnits='userSpaceOnUse' color-interpolation-filters='linearRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.102' numOctaves='4' seed='15' stitchTiles='stitch' x='0%25' y='0%25' width='100%25' height='100%25' result='turbulence'%3E%3C/feTurbulence%3E%3CfeSpecularLighting surfaceScale='15' specularConstant='0.75' specularExponent='20' lighting-color='%23f5a623' x='0%25' y='0%25' width='100%25' height='100%25' in='turbulence' result='specularLighting'%3E%3CfeDistantLight azimuth='3' elevation='100'%3E%3C/feDistantLight%3E%3C/feSpecularLighting%3E%3CfeColorMatrix type='saturate' values='0' x='0%25' y='0%25' width='100%25' height='100%25' in='specularLighting' result='colormatrix'%3E%3C/feColorMatrix%3E%3C/filter%3E%3C/defs%3E%3Crect width='700' height='700' fill='transparent'%3E%3C/rect%3E%3Crect width='700' height='700' fill='%23f5a623' filter='url(%23nnnoise-filter)'%3E%3C/rect%3E%3C/svg%3E")
.mh-0
min-height: 0 !important

View File

@ -4,6 +4,8 @@
Active Sessions
{% endblock %}
{% block column_class %}col{% endblock column_class %}
{% block body %}
<div class="bid box">
<h2>Active Sessions</h2>
@ -30,10 +32,10 @@ Active Sessions
</td>
<td>{{ session.ip }}</td>
<td>{{ session.device }}</td>
<td class="text-center">
<form action="{% url 'bid_main:terminate_session' session.pk %}" method="post">
<td>
<form action="{% url 'bid_main:terminate_session' session.pk %}" class="d-flex justify-content-end" method="post">
{% csrf_token %}
<button type="submit" class="btn-danger" title="Terminate Session"><i class="i-trash"></i></button>
<button type="submit" class="btn btn-danger mh-0" title="Terminate Session"><i class="i-trash"></i></button>
</form>
</td>
</tr>

View File

@ -1,5 +1,5 @@
<div class="col">
<div class="oauth-background mb-4" style="background-image:url({{ oauth_app.background.url }})">
<div class="oauth-bg mb-4" style="background-image:url({{ oauth_app.background.url }})">
<div class="row">
<div class="col-md-7 col-sm-6 mx-auto d-flex flex-column align-items-sm-start align-items-center justify-content-center text-center text-sm-left mb-sm-0">

View File

@ -1,5 +1,5 @@
<div class="bid-cta">
<div><p><i class="icon mr-2">
<div><p><i class="icon me-2">
<svg class="bi" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"></path>
<path d="M8.93 6.588l-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"></path>

View File

@ -39,7 +39,7 @@ Profile
{% endif %}
<div class="bid-user">
<a class="mr-4" href="{% url 'bid_main:profile' %}">{% avatar %}</a>
<a class="me-4" href="{% url 'bid_main:profile' %}">{% avatar %}</a>
<div class="bid-user-info">
{% if not user.full_name %}

View File

@ -8,8 +8,7 @@
Before you continue to use our service, please read and agree
to <a href="/privacy-policy" target="_blank">our Privacy Policy</a>.
</p>
<div class="my-3 text-center">
<form role="login" action="" method="POST">
<form role="login" action="" class="my-3" method="POST">
{% csrf_token %}
{{ form.next_url }}
<fieldset>{{ form.agree }}
@ -19,7 +18,6 @@
</div>
</fieldset>
</form>
</div>
<hr/>

View File

@ -40,8 +40,10 @@
<!-- Avatar input-->
{% avatar size=128 %}
<div class="custom-file my-3">
<input class="custom-file-input" id="avatar-file-input" type="file" name="avatar"/>
<div class="d-flex form-group my-3">
<span class="form-group-addon"><i class="i-image"></i></span>
<input class="form-control" id="avatar-file-input" name="avatar" type="file">
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="avatar-clear"/>
@ -54,7 +56,7 @@
<span>Cancel</span>
</a>
{% if user.is_staff %}
<a class="mr-auto btn btn-admin" href="{% url 'admin:bid_main_user_change' user.id %}">
<a class="me-auto btn btn-admin" href="{% url 'admin:bid_main_user_change' user.id %}">
Admin
</a>
{% endif %}

View File

@ -1,7 +1,7 @@
<li class="badge js-badge {{ badge.name }}{% if badge.id in private_badge_ids %} is-private{% endif %}" title="{{ badge.description }}">
{% if badge.badge_img %}
<figure class="badge-image">
<figure class="badge-img">
<img src="{{ badge.badge_img.url }}" alt="{{ badge.description }}" width="{{ badge.badge_img_width }}" height="{{ badge.badge_img_height }}"/>
</figure>
{% endif %}

View File

@ -2,11 +2,11 @@
<label class="{{ label_class }}" for="{{ field.id_for_label }}">
<span>{% firstof label field.label %}</span>
{% if field.help_text and field.errors %}
<span class="small subtitle ml-2">{{ field.help_text }}</span>
<span class="small subtitle ms-2">{{ field.help_text }}</span>
{% endif %}
{% if field.field.required %}
<span class="form-required-indicator ml-1">*</span>
<span class="form-required-indicator ms-1">*</span>
{% endif %}
</label>
{% endspaceless %}

View File

@ -16,15 +16,17 @@ import pathlib
import pytz
import sys
from dotenv import load_dotenv
from django.urls import reverse_lazy
import dj_database_url
try:
from dotenv import load_dotenv
# Load variables from .env, if available
path = os.path.dirname(os.path.abspath(__file__)) + '/../.env'
if os.path.isfile(path):
load_dotenv(path)
except ImportError: # This is expected: there should be no python-dotenv in production
pass
BASE_DIR = pathlib.Path(__file__).absolute().parent.parent
@ -336,7 +338,7 @@ OAUTH2_PROVIDER['ALLOWED_REDIRECT_URI_SCHEMES'] = ['http', 'https']
LOGGING = {
"version": 1,
"disable_existing_loggers": True,
"disable_existing_loggers": False,
"formatters": {
"default": {"format": "%(asctime)-15s %(levelname)8s %(name)s %(message)s"},
"verbose": {
@ -349,6 +351,11 @@ LOGGING = {
"formatter": "default", # Set to 'verbose' in production
"stream": "ext://sys.stderr",
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'include_html': True,
},
},
"loggers": {
"bid_main": {"level": "DEBUG"},
@ -360,6 +367,7 @@ LOGGING = {
"level": "WARNING",
"handlers": [
"console",
"mail_admins",
],
},
}
@ -413,3 +421,10 @@ if os.environ.get('EMAIL_HOST_USER') is not None:
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
if os.environ.get('EMAIL_HOST_PASSWORD') is not None:
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
if os.environ.get('ADMINS') is not None:
# Expects the following format:
# ADMINS='J Doe: jane@example.com, John Dee: john@example.com'
ADMINS = [[_.strip() for _ in adm.split(':')] for adm in os.environ.get('ADMINS').split(',')]
EMAIL_SUBJECT_PREFIX = f'[{ALLOWED_HOSTS[0]}]'
SERVER_EMAIL = f'django@{ALLOWED_HOSTS[0]}'

View File

@ -1,5 +1,8 @@
env: production
domain: id.blender.org
# Production uWSGI app currently runs on the same server
single_host: true
mailto: sintel-cron-jobs+bid@blender.org
host: web-production-1.hz-nbg1.blender.internal
db_host: db-postgres-production-1.hz-nbg1.blender.internal
ssl_only: true
admins: 'Anna Sirota: anna@blender.org'

View File

@ -1,8 +1,8 @@
---
application:
hosts:
sintel.blender.org:
ingress:
hosts:
sintel.blender.org:
lb-production-1.hz-nbg1.blender.internal:
application:
hosts:
web-production-1.hz-nbg1.blender.internal:

View File

@ -0,0 +1 @@
../../../../vars_common.yaml

View File

@ -0,0 +1,10 @@
env: staging
host: web-staging-1.hz-nbg1.blender.internal
db_host: db-postgres-staging-1.hz-nbg1.blender.internal
db_name: id_staging
db_user: id_staging
branch: main
ssl_only: true
backup_dir: /data/backups

View File

@ -0,0 +1,8 @@
---
ingress:
hosts:
lb-staging-1.hz-nbg1.blender.internal:
application:
hosts:
web-staging-1.hz-nbg1.blender.internal:

View File

@ -1,10 +1,4 @@
env: staging
host: web-staging-1.hz-nbg1.blender.internal
db_host: db-postgres-staging-1.hz-nbg1.blender.internal
db_name: id_staging
db_user: id_staging
host: web-1.internal
domain: staging.id.blender.org
branch: main
ssl_only: true
backup_dir: /data/backups

View File

@ -1,8 +1,8 @@
---
ingress:
hosts:
lb-staging-1.hz-nbg1.blender.internal:
lb-1:
application:
hosts:
web-staging-1.hz-nbg1.blender.internal:
web-1:

@ -1 +1 @@
Subproject commit 2e9a85dc8b2f9bd3cb807de5a489615355b50f70
Subproject commit d02f853634c2e490aa0eed3416ea3f63113ab60d

View File

@ -36,21 +36,19 @@ rate_limit:
burst: 50
delay: 10
aliases: null # This project doesn't use cron
certbot:
email: root@blender.org
source_url: https://projects.blender.org/infrastructure/{{ project_slug }}.git
branch: production
backup_dir: /mnt/backup
ssl_only: false
ca_certificate: /usr/local/share/ca-certificates/cloud-init-ca-cert-1.crt
include_common_services:
- background
- background-restart
- backup-service-dirs
- clearsessions
- delete-completed-tasks
- notify-email@

View File

@ -46,7 +46,6 @@ pynacl==1.5.0
pypng==0.20220715.0
pypugjs==5.9.12 ; python_version >= "3.8" and python_version < "4"
python-dateutil==2.8.1 ; python_version >= "3.8" and python_version < "4"
python-dotenv==0.21.0
pytz==2019.3 ; python_version >= "3.8" and python_version < "4"
pyyaml==5.1.2 ; python_version >= "3.8" and python_version < "4"
qrcode==7.4.2

View File

@ -9,6 +9,7 @@ freezegun==1.3.1
mccabe==0.7.0
pycodestyle==2.11.1
pyflakes==3.1.0
python-dotenv==0.21.0
responses==0.24.1
tblib==3.0.0
typing_extensions==4.9.0

View File

@ -79,7 +79,7 @@
</div>
{% get_flatpages as flatpages %}
{% if flatpages %}
<div class="footer-pages pt-2">
<div class="footer-pages">
<div class="container">
<ul class="d-flex justify-content-center">
{% for page in flatpages %}

File diff suppressed because one or more lines are too long