Modified login app to work in dashboard context
This commit is contained in:
parent
e8063b1de7
commit
755cb03aaf
36 changed files with 22251 additions and 24 deletions
5
app.py
5
app.py
|
@ -26,10 +26,14 @@ from helpers import (
|
|||
KratosUser
|
||||
)
|
||||
from config import *
|
||||
import logging
|
||||
|
||||
app = Flask(__name__)
|
||||
cors = CORS(app)
|
||||
app.config["SECRET_KEY"] = SECRET_KEY
|
||||
|
||||
app.logger.setLevel(logging.INFO)
|
||||
|
||||
app.register_blueprint(api_v1)
|
||||
app.register_blueprint(web)
|
||||
|
||||
|
@ -42,7 +46,6 @@ app.register_error_handler(HydraError, hydra_error)
|
|||
|
||||
jwt = JWTManager(app)
|
||||
|
||||
|
||||
# When token is not valid or missing handler
|
||||
@jwt.invalid_token_loader
|
||||
@jwt.unauthorized_loader
|
||||
|
|
|
@ -2,7 +2,7 @@ from flask import Blueprint
|
|||
|
||||
api_v1 = Blueprint("api_v1", __name__, url_prefix="/api/v1")
|
||||
web = Blueprint("web", __name__, url_prefix="/web")
|
||||
|
||||
# cli = Blueprint('cli', __name__)
|
||||
|
||||
@api_v1.route("/")
|
||||
@api_v1.route("/health")
|
||||
|
|
|
@ -30,6 +30,19 @@ from ory_kratos_client.api import v0alpha2_api as kratos_api
|
|||
|
||||
from areas import web
|
||||
from config import *
|
||||
from flask import current_app
|
||||
|
||||
from helpers import (
|
||||
BadRequest,
|
||||
KratosError,
|
||||
HydraError,
|
||||
bad_request_error,
|
||||
validation_error,
|
||||
kratos_error,
|
||||
global_error,
|
||||
hydra_error,
|
||||
KratosUser
|
||||
)
|
||||
|
||||
# APIs
|
||||
# Create HYDRA & KRATOS API interfaces
|
||||
|
@ -143,7 +156,7 @@ def auth():
|
|||
challenge = request.args.post("login_challenge")
|
||||
|
||||
if not challenge:
|
||||
app.logger.error("No challenge given. Error in request")
|
||||
current_app.logger.error("No challenge given. Error in request")
|
||||
abort(400, description="Challenge required when requesting authorization")
|
||||
|
||||
|
||||
|
@ -160,25 +173,25 @@ def auth():
|
|||
url = PUBLIC_URL + "/auth?login_challenge=" + challenge
|
||||
url = urllib.parse.quote_plus(url)
|
||||
|
||||
app.logger.info("Redirecting to login. Setting flow_state cookies")
|
||||
app.logger.info("auth_url: " + url)
|
||||
current_app.logger.info("Redirecting to login. Setting flow_state cookies")
|
||||
current_app.logger.info("auth_url: " + url)
|
||||
|
||||
response = redirect(app.config["PUBLIC_URL"] + "/login")
|
||||
response = redirect(PUBLIC_URL + "/login")
|
||||
response.set_cookie('flow_state', 'auth')
|
||||
response.set_cookie('auth_url', url)
|
||||
return response
|
||||
|
||||
|
||||
|
||||
app.logger.info("User is logged in. We can authorize the user")
|
||||
current_app.logger.info("User is logged in. We can authorize the user")
|
||||
|
||||
try:
|
||||
login_request = HYDRA.login_request(challenge)
|
||||
except hydra_client.exceptions.NotFound:
|
||||
app.logger.error(f"Not Found. Login request not found. challenge={challenge}")
|
||||
current_app.logger.error(f"Not Found. Login request not found. challenge={challenge}")
|
||||
abort(404, description="Login request not found. Please try again.")
|
||||
except hydra_client.exceptions.HTTPError:
|
||||
app.logger.error(f"Conflict. Login request has been used already. challenge={challenge}")
|
||||
current_app.logger.error(f"Conflict. Login request has been used already. challenge={challenge}")
|
||||
abort(503, description="Login request already used. Please try again.")
|
||||
|
||||
# Authorize the user
|
||||
|
@ -208,10 +221,10 @@ def consent():
|
|||
try:
|
||||
consent_request = HYDRA.consent_request(challenge)
|
||||
except hydra_client.exceptions.NotFound:
|
||||
app.logger.error(f"Not Found. Consent request {challenge} not found")
|
||||
current_app.logger.error(f"Not Found. Consent request {challenge} not found")
|
||||
abort(404, description="Consent request does not exist. Please try again")
|
||||
except hydra_client.exceptions.HTTPError:
|
||||
app.logger.error(f"Conflict. Consent request {challenge} already used")
|
||||
current_app.logger.error(f"Conflict. Consent request {challenge} already used")
|
||||
abort(503, description="Consent request already used. Please try again")
|
||||
|
||||
# Get information about this consent request:
|
||||
|
@ -223,11 +236,12 @@ def consent():
|
|||
# Get the related user object
|
||||
user = KratosUser(KRATOS_ADMIN, kratos_id)
|
||||
if not user:
|
||||
app.logger.error(f"User not found in database: {kratos_id}")
|
||||
current_app.logger.error(f"User not found in database: {kratos_id}")
|
||||
abort(401, description="User not found. Please try again.")
|
||||
|
||||
# Get role on this app
|
||||
app_obj = db.session.query(App).filter(App.slug == app_id).first()
|
||||
#app_obj = db.session.query(App).filter(App.slug == app_id).first()
|
||||
app_obj = False
|
||||
|
||||
# Default access level
|
||||
roles = []
|
||||
|
@ -239,7 +253,7 @@ def consent():
|
|||
)
|
||||
for role_obj in role_objects:
|
||||
roles.append(role_obj.role)
|
||||
app.logger.info(f"Using '{roles}' when applying consent for {kratos_id}")
|
||||
current_app.logger.info(f"Using '{roles}' when applying consent for {kratos_id}")
|
||||
|
||||
# Get claims for this user, provided the current app
|
||||
claims = user.get_claims(app_id, roles)
|
||||
|
@ -247,8 +261,8 @@ def consent():
|
|||
# pylint: disable=fixme
|
||||
# TODO: Need to implement checking claims here, once the backend for that is
|
||||
# developed
|
||||
app.logger.info(f"Providing consent to {app_id} for {kratos_id}")
|
||||
app.logger.info(f"{kratos_id} was granted access to {app_id}")
|
||||
current_app.logger.info(f"Providing consent to {app_id} for {kratos_id}")
|
||||
current_app.logger.info(f"{kratos_id} was granted access to {app_id}")
|
||||
|
||||
# False positive: pylint: disable=no-member
|
||||
return redirect(consent_request.accept(
|
||||
|
@ -285,7 +299,7 @@ def get_auth():
|
|||
cookie = request.cookies.get('ory_kratos_session')
|
||||
cookie = "ory_kratos_session=" + cookie
|
||||
except TypeError:
|
||||
app.logger.info("User not logged in or cookie corrupted")
|
||||
current_app.logger.info("User not logged in or cookie corrupted")
|
||||
return False
|
||||
|
||||
# Given a cookie, check if it is valid and get the profile
|
||||
|
@ -297,7 +311,7 @@ def get_auth():
|
|||
return api_response.identity
|
||||
|
||||
except ory_kratos_client.ApiException as error:
|
||||
app.logger.error(f"Exception when calling V0alpha2Api->to_session(): {error}\n")
|
||||
current_app.logger.error(f"Exception when calling V0alpha2Api->to_session(): {error}\n")
|
||||
|
||||
return False
|
||||
|
||||
|
|
|
@ -29,10 +29,10 @@ export HYDRA_AUTHORIZATION_BASE_URL="https://sso.init.stackspin.net/oauth2/auth"
|
|||
export TOKEN_URL="https://sso.init.stackspin.net/oauth2/token"
|
||||
|
||||
# Login facilitator paths
|
||||
export KRATOS_PUBLIC_URL=http://localhost/kapi
|
||||
export KRATOS_PUBLIC_URL=http://localhost/kratos
|
||||
export KRATOS_ADMIN_URL=http://localhost:8000
|
||||
export HYDRA_ADMIN_URL=http://localhost:4445
|
||||
export PUBLIC_URL=http://localhost/login
|
||||
export PUBLIC_URL=http://localhost/web/
|
||||
export DATABASE_URL="mysql+pymysql://stackspin:stackspin@localhost/stackspin?charset=utf8mb4"
|
||||
|
||||
|
||||
|
|
|
@ -22,10 +22,10 @@ fi
|
|||
admin=`ssh $host -lroot kubectl get service -n $namespace |grep single-sign-on-kratos-admin | awk '{print $3'}`
|
||||
public=`ssh $host -lroot kubectl get service -n $namespace |grep single-sign-on-kratos-public | awk '{print $3}'`
|
||||
hydra=`ssh $host -lroot kubectl get service -n $namespace |grep single-sign-on-hydra-admin | awk '{print $3}'`
|
||||
psql=`ssh $host -lroot kubectl get service -n $namespace |grep single-sign-on-database-postgres|grep -v headless | awk '{print $3}'`
|
||||
mysql=`ssh $host -lroot kubectl get service -n $namespace |grep single-sign-on-database-maria|grep -v headless | awk '{print $3}'`
|
||||
|
||||
|
||||
if [ "x$admin" == 'x' ] || [ "x$public" == 'x' ] || [ "x$hydra" == 'x' ] || [ "x$psql" == 'x' ]
|
||||
if [ "x$admin" == 'x' ] || [ "x$public" == 'x' ] || [ "x$hydra" == 'x' ] || [ "x$mysql" == 'x' ]
|
||||
then
|
||||
echo "It seems we where not able find at least one of the remote services"
|
||||
echo " please make sure that kubectl use the right namespace by default."
|
||||
|
@ -39,7 +39,7 @@ echo "
|
|||
kratos admin port will be at localhost: 8000
|
||||
kratos public port will be at localhost: 8080
|
||||
hydra admin port will be at localhost: 4445
|
||||
psql port will be at localhost: 5432
|
||||
mysql port will be at localhost: 3306
|
||||
"
|
||||
|
||||
ssh -L 8000:$admin:80 -L 8080:$public:80 -L 4445:$hydra:4445 -L 5432:$psql:5432 root@$host
|
||||
ssh -L 8000:$admin:80 -L 8080:$public:80 -L 4445:$hydra:4445 -L 3306:$mysql:3306 root@$host
|
0
static/.gitkeep
Normal file
0
static/.gitkeep
Normal file
410
static/base.js
Normal file
410
static/base.js
Normal file
|
@ -0,0 +1,410 @@
|
|||
|
||||
|
||||
/* base.js
|
||||
This is the base JS file to render the user interfaces of kratos and provide
|
||||
the end user with flows for login, recovery etc.
|
||||
|
||||
check_flow_*():
|
||||
These functions check the status of the flow and based on the status do some
|
||||
action to get a better experience for the end user. Usually this is a
|
||||
redirect based on the state
|
||||
|
||||
flow_*():
|
||||
execute / render all UI elements in a flow. Kratos expects you to work on
|
||||
to query kratos which provides you with the UI elements needed to be
|
||||
rendered. This querying and rendering is done exectly by those function.
|
||||
Based on what kratos provides or the state of the flow, elements are maybe
|
||||
hidden or shown
|
||||
|
||||
*/
|
||||
|
||||
|
||||
// Check if an auth flow is configured and redirect to auth page in that
|
||||
// case.
|
||||
function check_flow_auth() {
|
||||
var state = Cookies.get('flow_state');
|
||||
var url = Cookies.get('auth_url');
|
||||
|
||||
if (state == 'auth') {
|
||||
Cookies.set('flow_state','');
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there if the flow is expired, if so, reset the cookie
|
||||
function check_flow_expired() {
|
||||
var state = Cookies.get('flow_state');
|
||||
|
||||
if (state == 'flow_expired') {
|
||||
Cookies.set('flow_state','');
|
||||
$("#contentFlowExpired").show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// The script executed on login flows
|
||||
function flow_login() {
|
||||
|
||||
var flow = $.urlParam('flow');
|
||||
var uri = api_url + 'self-service/login/flows?id=' + flow;
|
||||
|
||||
// Query the Kratos backend to know what fields to render for the
|
||||
// current flow
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: uri,
|
||||
success: function(data) {
|
||||
|
||||
// Render login form (group: password)
|
||||
var html = render_form(data, 'password');
|
||||
$("#contentLogin").html(html);
|
||||
|
||||
},
|
||||
complete: function(obj) {
|
||||
|
||||
// If we get a 410, the flow is expired, need to refresh the flow
|
||||
if (obj.status == 410) {
|
||||
Cookies.set('flow_state','flow_expired');
|
||||
// If we call the page without arguments, we get a new flow
|
||||
window.location.href = 'login';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This is called after a POST on settings. It tells if the save was
|
||||
// successful and display / handles based on that outcome
|
||||
function flow_settings_validate() {
|
||||
|
||||
var flow = $.urlParam('flow');
|
||||
var uri = api_url + 'self-service/settings/flows?id=' + flow;
|
||||
|
||||
$.ajax( {
|
||||
type: "GET",
|
||||
url: uri,
|
||||
success: function(data) {
|
||||
|
||||
// We had success. We save that fact in our flow_state
|
||||
// cookie and regenerate a new flow
|
||||
if (data.state == 'success') {
|
||||
Cookies.set('flow_state', 'settings_saved');
|
||||
|
||||
// Redirect to generate new flow ID
|
||||
window.location.href = 'settings';
|
||||
}
|
||||
else {
|
||||
|
||||
// There was an error, Kratos does not specify what is
|
||||
// wrong. So we just show the general error message and
|
||||
// let the user figure it out. We can re-use the flow-id
|
||||
$("#contentProfileSaveFailed").show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Render the settings flow, this is where users can change their personal
|
||||
// settings, like name and password. The form contents are defined by Kratos
|
||||
function flow_settings() {
|
||||
|
||||
// Get the details from the current flow from kratos
|
||||
var flow = $.urlParam('flow');
|
||||
var uri = api_url + 'self-service/settings/flows?id=' + flow;
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: uri,
|
||||
success: function(data) {
|
||||
|
||||
var state = Cookies.get('flow_state')
|
||||
|
||||
// If we have confirmation the settings are saved, show the
|
||||
// notification
|
||||
if (state == 'settings_saved') {
|
||||
$("#contentProfileSaved").show();
|
||||
Cookies.set('flow_state', 'settings');
|
||||
}
|
||||
|
||||
// Hide prfile section if we are in recovery state
|
||||
// so the user is not confused by other fields. The user
|
||||
// probably want to setup a password only first.
|
||||
if (state == 'recovery') {
|
||||
$("#contentProfile").hide();
|
||||
}
|
||||
|
||||
|
||||
// Render the password & profile form based on the fields we got
|
||||
// from the API
|
||||
var html = render_form(data, 'password');
|
||||
$("#contentPassword").html(html);
|
||||
|
||||
html = render_form(data, 'profile');
|
||||
$("#contentProfile").html(html);
|
||||
|
||||
// If the submit button is hit, execute the POST with Ajax.
|
||||
$("#formpassword").submit(function(e) {
|
||||
|
||||
// avoid to execute the actual submit of the form.
|
||||
e.preventDefault();
|
||||
|
||||
var form = $(this);
|
||||
var url = form.attr('action');
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: form.serialize(),
|
||||
complete: function(obj) {
|
||||
// Validate the settings
|
||||
flow_settings_validate();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
},
|
||||
complete: function(obj) {
|
||||
|
||||
// If we get a 410, the flow is expired, need to refresh the flow
|
||||
if (obj.status == 410) {
|
||||
Cookies.set('flow_state','flow_expired');
|
||||
window.location.href = 'settings';
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function flow_recover() {
|
||||
var flow = $.urlParam('flow');
|
||||
var uri = api_url + 'self-service/recovery/flows?id=' + flow;
|
||||
|
||||
$.ajax( {
|
||||
type: "GET",
|
||||
url: uri,
|
||||
success: function(data) {
|
||||
|
||||
// Render the recover form, method 'link'
|
||||
var html = render_form(data, 'link');
|
||||
$("#contentRecover").html(html);
|
||||
|
||||
// Do form post as an AJAX call
|
||||
$("#formlink").submit(function(e) {
|
||||
|
||||
// avoid to execute the actual submit of the form.
|
||||
e.preventDefault();
|
||||
|
||||
var form = $(this);
|
||||
var url = form.attr('action');
|
||||
|
||||
// keep stat we are in recovery
|
||||
Cookies.set('flow_state', 'recovery');
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: form.serialize(), // serializes the form's elements.
|
||||
success: function(data)
|
||||
{
|
||||
|
||||
// Show the request is sent out
|
||||
$("#contentRecover").hide();
|
||||
$("#contentRecoverRequested").show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
complete: function(obj) {
|
||||
|
||||
// If we get a 410, the flow is expired, need to refresh the flow
|
||||
if (obj.status == 410) {
|
||||
Cookies.set('flow_state','flow_expired');
|
||||
window.location.href = 'recovery';
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Based on Kratos UI data and a group name, get the full form for that group.
|
||||
// kratos groups elements which belongs together in a group and should be posted
|
||||
// at once. The elements in the default group should be part of all other
|
||||
// groups.
|
||||
//
|
||||
// data: data object as returned form the API
|
||||
// group: group to render.
|
||||
function render_form(data, group) {
|
||||
|
||||
// Create form
|
||||
var action = data.ui.action;
|
||||
var method = data.ui.method;
|
||||
var form = "<form id='form"+group+"' method='"+method+"' action='"+action+"'>";
|
||||
|
||||
for (const node of data.ui.nodes) {
|
||||
|
||||
var name = node.attributes.name;
|
||||
var type = node.attributes.type;
|
||||
var value = node.attributes.value;
|
||||
|
||||
if (node.group == 'default' || node.group == group) {
|
||||
var elm = getFormElement(type, name, value);
|
||||
form += elm;
|
||||
}
|
||||
}
|
||||
form += "</form>";
|
||||
return form;
|
||||
|
||||
}
|
||||
|
||||
// Return form element based on name, including help text (sub), placeholder etc.
|
||||
// Kratos give us form names and types and specifies what to render. However
|
||||
// it does not provide labels or translations. This function returns a HTML
|
||||
// form element based on the fields provided by Kratos with proper names and
|
||||
// labels
|
||||
// type: input type, usual "input", "hidden" or "submit". But bootstrap types
|
||||
// like "email" are also supported
|
||||
// name: name of the field. Used when posting data
|
||||
// value: If there is already a value known, show it
|
||||
function getFormElement(type, name, value) {
|
||||
|
||||
if (value == undefined) {
|
||||
value = '';
|
||||
}
|
||||
if (name == 'email' || name == 'traits.email') {
|
||||
return getFormInput(
|
||||
'email',
|
||||
name,
|
||||
value,
|
||||
'E-mail address',
|
||||
'Please enter your e-mail address here',
|
||||
'Please provide your e-mail address. We will send a recovery ' +
|
||||
'link to that e-mail address.',
|
||||
);
|
||||
}
|
||||
|
||||
if (name == 'traits.username') {
|
||||
return getFormInput(
|
||||
'name',
|
||||
name,
|
||||
value,
|
||||
'Username',
|
||||
'Please provide an username',
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (name == 'traits.name') {
|
||||
return getFormInput(
|
||||
'name',
|
||||
name,
|
||||
value,
|
||||
'Full name',
|
||||
'Please provide your full name',
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (name == 'password_identifier') {
|
||||
return getFormInput(
|
||||
'email',
|
||||
name,
|
||||
value,
|
||||
'E-mail address',
|
||||
'Please provide your e-mail address to login',
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (name == 'password') {
|
||||
return getFormInput(
|
||||
'password',
|
||||
name,
|
||||
value,
|
||||
'Password',
|
||||
'Please provide your password',
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (type == 'hidden' || name == 'traits.uuid') {
|
||||
|
||||
return `
|
||||
<input type="hidden" class="form-control" id="`+name+`"
|
||||
name="`+name+`" value='`+value+`'>`;
|
||||
}
|
||||
|
||||
if (type == 'submit') {
|
||||
|
||||
return `<div class="form-group">
|
||||
<input type="hidden" name="`+name+`" value="`+value+`">
|
||||
<button type="submit" class="btn btn-primary">Go!</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
return getFormInput('input', name, value, name, null,null);
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Usually called by getFormElement, generic function to generate an
|
||||
// input box.
|
||||
// param type: type of input, like 'input', 'email', 'password'
|
||||
// param name: name of form field, used when posting the form
|
||||
// param value: preset value of the field
|
||||
// param label: Label to display above field
|
||||
// param placeHolder: Label to display in field if empty
|
||||
// param help: Additional help text, displayed below the field in small font
|
||||
function getFormInput(type, name, value, label, placeHolder, help) {
|
||||
|
||||
// Id field for help element
|
||||
var nameHelp = name + "Help";
|
||||
|
||||
var element = '<div class="form-group">';
|
||||
element += '<label for="'+name+'">'+label+'</label>';
|
||||
element += '<input type="'+type+'" class="form-control" id="'+name+'" name="'+name+'" ';
|
||||
|
||||
// If we are a password field, add a eye icon to reveal password
|
||||
if (value) {
|
||||
element += 'value="'+value+'" ';
|
||||
}
|
||||
if (help) {
|
||||
element += 'aria-describedby="' + nameHelp +'" ';
|
||||
}
|
||||
if (placeHolder) {
|
||||
element += 'placeholder="'+placeHolder+'" ';
|
||||
}
|
||||
element += ">";
|
||||
|
||||
if (help) {
|
||||
element +=
|
||||
`<small id="`+nameHelp+`" class="form-text text-muted">` + help + `
|
||||
</small>`;
|
||||
}
|
||||
|
||||
element += '</div>';
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// $.urlParam get parameters from the URI. Example: id = $.urlParam('id');
|
||||
$.urlParam = function(name) {
|
||||
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
|
||||
if (results==null) {
|
||||
return null;
|
||||
}
|
||||
return decodeURI(results[1]) || 0;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
2050
static/css/bootstrap-grid.css
vendored
Normal file
2050
static/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
static/css/bootstrap-grid.css.map
Normal file
1
static/css/bootstrap-grid.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/css/bootstrap-grid.min.css
vendored
Normal file
7
static/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap-grid.min.css.map
Normal file
1
static/css/bootstrap-grid.min.css.map
Normal file
File diff suppressed because one or more lines are too long
330
static/css/bootstrap-reboot.css
vendored
Normal file
330
static/css/bootstrap-reboot.css
vendored
Normal file
|
@ -0,0 +1,330 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-ms-overflow-style: scrollbar;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
@-ms-viewport {
|
||||
width: device-width;
|
||||
}
|
||||
|
||||
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-original-title] {
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: .5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
-webkit-text-decoration-skip: objects;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
-ms-overflow-style: scrollbar;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
button,
|
||||
html [type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
-webkit-appearance: listbox;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type="search"] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
[type="search"]::-webkit-search-cancel-button,
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
1
static/css/bootstrap-reboot.css.map
Normal file
1
static/css/bootstrap-reboot.css.map
Normal file
File diff suppressed because one or more lines are too long
8
static/css/bootstrap-reboot.min.css
vendored
Normal file
8
static/css/bootstrap-reboot.min.css
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
1
static/css/bootstrap-reboot.min.css.map
Normal file
1
static/css/bootstrap-reboot.min.css.map
Normal file
File diff suppressed because one or more lines are too long
8975
static/css/bootstrap.css
vendored
Normal file
8975
static/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
static/css/bootstrap.css.map
Normal file
1
static/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/css/bootstrap.min.css
vendored
Normal file
7
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap.min.css.map
Normal file
1
static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
6328
static/js/bootstrap.bundle.js
vendored
Normal file
6328
static/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
static/js/bootstrap.bundle.js.map
Normal file
1
static/js/bootstrap.bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/bootstrap.bundle.min.js.map
Normal file
1
static/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
3894
static/js/bootstrap.js
vendored
Normal file
3894
static/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
static/js/bootstrap.js.map
Normal file
1
static/js/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
7
static/js/bootstrap.min.js
vendored
Normal file
7
static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/bootstrap.min.js.map
Normal file
1
static/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
static/js/jquery-3.6.0.min.js
vendored
Normal file
2
static/js/jquery-3.6.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
static/js/js.cookie.min.js
vendored
Normal file
2
static/js/js.cookie.min.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/*! js-cookie v3.0.1 | MIT */
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}return function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"})}));
|
9
static/logo.svg
Normal file
9
static/logo.svg
Normal file
|
@ -0,0 +1,9 @@
|
|||
<svg width="151" height="44" viewBox="0 0 151 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="151" height="44" fill="transparent"/>
|
||||
<path d="M40.8246 11.7373C37.2206 11.7373 34.89 13.5633 34.89 16.6388C34.89 19.7142 36.8842 20.8915 40.4162 21.6604L40.9688 21.7805C43.1312 22.2611 44.2604 22.7416 44.2604 24.1351C44.2604 25.4806 43.1792 26.4417 41.0168 26.4417C38.8544 26.4417 37.5329 25.4326 37.5329 23.4143V23.1741H34.4094V23.4143C34.4094 27.0664 37.1245 29.2288 41.0168 29.2288C44.9092 29.2288 47.3839 27.1145 47.3839 24.039C47.3839 20.9636 45.1014 19.7142 41.5214 18.9454L40.9688 18.8252C38.9025 18.3687 38.0135 17.7921 38.0135 16.5427C38.0135 15.2933 38.9025 14.5244 40.8246 14.5244C42.7468 14.5244 43.9241 15.2933 43.9241 17.2154V17.5998H47.0476V17.2154C47.0476 13.5633 44.4286 11.7373 40.8246 11.7373ZM47.5746 19.4259H50.554V26.2015C50.554 27.8353 51.6112 28.8925 53.1969 28.8925H56.5607V26.4417H54.2541C53.8216 26.4417 53.5814 26.2015 53.5814 25.7209V19.4259H56.849V16.9752H53.5814V13.275H50.554V16.9752H47.5746V19.4259ZM69.9725 28.8925V16.9752H66.9932V18.5849H66.7529C66.0321 17.5518 64.8788 16.6388 62.8125 16.6388C59.9774 16.6388 57.4305 19.0415 57.4305 22.9338C57.4305 26.8261 59.9774 29.2288 62.8125 29.2288C64.8788 29.2288 66.0321 28.3158 66.7529 27.2827H66.9932V28.8925H69.9725ZM63.7256 26.6339C61.8515 26.6339 60.4579 25.2884 60.4579 22.9338C60.4579 20.5792 61.8515 19.2337 63.7256 19.2337C65.5996 19.2337 66.9932 20.5792 66.9932 22.9338C66.9932 25.2884 65.5996 26.6339 63.7256 26.6339ZM71.4505 22.9338C71.4505 26.6339 74.1896 29.2288 77.6495 29.2288C80.9892 29.2288 82.9354 27.2827 83.6081 24.6637L80.6768 23.967C80.4126 25.5047 79.5236 26.5859 77.6975 26.5859C75.8715 26.5859 74.4779 25.2404 74.4779 22.9338C74.4779 20.6272 75.8715 19.2817 77.6975 19.2817C79.5236 19.2817 80.4126 20.435 80.5807 21.8766L83.512 21.2519C82.9834 18.5849 80.9652 16.6388 77.6495 16.6388C74.1896 16.6388 71.4505 19.2337 71.4505 22.9338ZM97.4406 16.9752H92.9716L88.0221 21.348V12.0737H84.9947V28.8925H88.0221V25.0241L89.68 23.6066L93.6204 28.8925H97.3445L91.8424 21.7565L97.4406 16.9752ZM98.2594 20.3149C98.2594 22.6695 100.23 23.5345 102.728 24.015L103.353 24.1351C104.843 24.4235 105.516 24.7839 105.516 25.5527C105.516 26.3216 104.843 26.9223 103.449 26.9223C102.056 26.9223 100.926 26.3456 100.614 24.6157L97.8269 25.3365C98.2354 27.8353 100.326 29.2288 103.449 29.2288C106.477 29.2288 108.447 27.8112 108.447 25.3125C108.447 22.8137 106.429 22.1409 103.738 21.6123L103.113 21.4922C101.863 21.2519 101.191 20.9156 101.191 20.1227C101.191 19.4019 101.815 18.9454 102.969 18.9454C104.122 18.9454 104.939 19.4259 105.227 20.7233L107.966 19.8824C107.39 17.9603 105.636 16.6388 102.969 16.6388C100.134 16.6388 98.2594 17.9603 98.2594 20.3149ZM109.985 33.6978H113.012V27.3547H113.252C113.925 28.3158 115.078 29.2288 117.145 29.2288C119.98 29.2288 122.527 26.8261 122.527 22.9338C122.527 19.0415 119.98 16.6388 117.145 16.6388C115.078 16.6388 113.925 17.5518 113.204 18.5849H112.964V16.9752H109.985V33.6978ZM116.231 26.6339C114.357 26.6339 112.964 25.2884 112.964 22.9338C112.964 20.5792 114.357 19.2337 116.231 19.2337C118.106 19.2337 119.499 20.5792 119.499 22.9338C119.499 25.2884 118.106 26.6339 116.231 26.6339ZM123.38 13.5153C123.38 14.7407 124.317 15.5816 125.518 15.5816C126.72 15.5816 127.657 14.7407 127.657 13.5153C127.657 12.2899 126.72 11.449 125.518 11.449C124.317 11.449 123.38 12.2899 123.38 13.5153ZM127.032 16.9752H124.005V28.8925H127.032V16.9752ZM129.249 16.9752V28.8925H132.277V22.7416C132.277 20.5311 133.358 19.2337 135.208 19.2337C136.842 19.2337 137.755 20.1227 137.755 21.9247V28.8925H140.782V21.7805C140.782 18.8252 138.932 16.7829 136.145 16.7829C133.958 16.7829 132.949 17.744 132.469 18.7531H132.228V16.9752H129.249Z" fill="#2D535A"/>
|
||||
<path d="M0 35.1171C0 39.0948 4.18188 41.6853 7.74332 39.9138L22.8687 32.39C24.0424 31.8062 24.7844 30.6083 24.7844 29.2975V20.9534L1.64288 32.4649C0.636359 32.9656 0 33.9929 0 35.1171Z" fill="#2D535A"/>
|
||||
<path d="M0 22.8091C0 27.6308 5.06914 30.7709 9.38621 28.6235L24.7844 20.9641C24.7844 16.1423 19.7151 13.0021 15.398 15.1496L0 22.8091Z" fill="#54C6CC"/>
|
||||
<path d="M2.2161 21.7068C0.858395 22.3821 -0.103566 23.8187 0.458285 25.2271C1.79955 28.5895 5.84578 30.3846 9.38621 28.6235L24.7844 20.9641C24.7844 16.1423 19.7151 13.0021 15.398 15.1496L2.2161 21.7068Z" fill="#2D535A"/>
|
||||
<path d="M2.2161 21.7068C0.858395 22.3821 -0.103566 23.8187 0.458285 25.2271C1.79955 28.5895 5.84578 30.3846 9.38621 28.6235L22.5683 22.0664C23.926 21.3911 24.888 19.9545 24.3261 18.546C22.9848 15.1836 18.9385 13.3884 15.398 15.1496L2.2161 21.7068Z" fill="#1E8290"/>
|
||||
<path d="M0 22.8121L23.3077 11.2182C24.2124 10.7682 24.7844 9.8448 24.7844 8.83432C24.7844 4.77111 20.5126 2.12495 16.8747 3.93462L2.25625 11.2064C0.873945 11.894 0 13.3048 0 14.8487V22.8121Z" fill="#54C6CC"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.7 KiB |
12
static/style.css
Normal file
12
static/style.css
Normal file
|
@ -0,0 +1,12 @@
|
|||
|
||||
|
||||
div.loginpanel {
|
||||
width: 644px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
40
templates/base.html
Normal file
40
templates/base.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/static/js/js.cookie.min.js"></script>
|
||||
<script src="/static/base.js"></script>
|
||||
<title>Stackspin Account</title>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
<body>
|
||||
|
||||
<script>
|
||||
var api_url = '{{ api_url }}';
|
||||
|
||||
// Actions
|
||||
$(document).ready(function() {
|
||||
check_flow_expired();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<div class="loginpanel">
|
||||
|
||||
<div id="contentFlowExpired"
|
||||
class='alert alert-warning'
|
||||
style='display:none'>Your request is expired. Please resubmit your request faster.</div>
|
||||
|
||||
|
||||
<img src='/static/logo.svg'/><br/><br/>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
|
||||
</div>
|
28
templates/loggedin.html
Normal file
28
templates/loggedin.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
var api_url = '{{ api_url }}';
|
||||
|
||||
// Actions
|
||||
$(document).ready(function() {
|
||||
//flow_login();
|
||||
check_flow_auth();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<div id="contentMessages"></div>
|
||||
<div id="contentWelcome">Welcome {{ id['name'] }},<br/><br/>
|
||||
You are already logged in.
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
25
templates/login.html
Normal file
25
templates/login.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
var api_url = '{{ api_url }}';
|
||||
|
||||
// Actions
|
||||
$(document).ready(function() {
|
||||
flow_login();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<div id="contentMessages"></div>
|
||||
<div id="contentLogin"></div>
|
||||
<div id="contentHelp">
|
||||
<a href='recovery'>Forget password?</a> | <a href='https://stackspin.net'>About stackspin</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
29
templates/recover.html
Normal file
29
templates/recover.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
|
||||
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
var api_url = '{{ api_url }}';
|
||||
|
||||
// Actions
|
||||
$(document).ready(function() {
|
||||
flow_recover();
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div id="contentMessages"></div>
|
||||
<div id="contentRecover"></div>
|
||||
<div id="contentRecoverRequested" style='display:none'>
|
||||
Thank you for your request. We have sent you an email to recover
|
||||
your account. Please check your e-mail and complete the account
|
||||
recovery. You have limited time to complete this</div>
|
||||
|
||||
<div id="contentHelp">
|
||||
<a href='login'>Back to login page</a> | <a href='https://stackspin.org'>About stackspin</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
30
templates/settings.html
Normal file
30
templates/settings.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
|
||||
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
var api_url = '{{ api_url }}';
|
||||
|
||||
// Actions
|
||||
$(document).ready(function() {
|
||||
flow_settings();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<div id="contentMessages"></div>
|
||||
<div id="contentProfileSaved"
|
||||
class='alert alert-success'
|
||||
style='display:none'>Successfuly saved new settings.</div>
|
||||
<div id="contentProfileSaveFailed"
|
||||
class='alert alert-danger'
|
||||
style='display:none'>Your changes are not saved. Please check the fields for errors.</div>
|
||||
<div id="contentProfile"></div>
|
||||
<div id="contentPassword"></div>
|
||||
|
||||
|
||||
{% endblock %}
|
Loading…
Reference in a new issue