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

Target

Select target project
  • xeruf/dashboard
  • stackspin/dashboard
2 results
Show changes
Showing
with 1191 additions and 608 deletions
"""Extend SCIM support to dynamic apps
Revision ID: 267d280db490
Revises: 825262488cd9
Create Date: 2024-04-12 11:49:00
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '267d280db490'
down_revision = '825262488cd9'
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"app",
sa.Column(
"scim_url",
sa.Unicode(length=1024),
nullable=True
),
)
op.add_column(
"app",
sa.Column(
"scim_token",
sa.Unicode(length=1024),
nullable=True
),
)
op.add_column(
"app",
sa.Column(
"scim_group_support",
sa.Boolean(),
server_default='0',
nullable=False
),
)
# ID of user in app for SCIM purposes. The dashboard needs this so it can
# construct the SCIM URL to the app identifying the user.
op.add_column(
"app_role",
sa.Column(
"scim_id",
sa.Unicode(length=256),
nullable=True
),
)
op.create_index(
"app_role__app_id__scim_id",
"app_role",
["app_id", "scim_id"],
unique=False
)
def downgrade():
op.drop_column("app", "scim_url")
op.drop_column("app", "scim_token")
op.drop_column("app", "scim_group_support")
op.drop_index("app_role__app_id__scim_id", "app_role")
op.drop_column("app_role", "scim_id")
......@@ -16,11 +16,11 @@ down_revision = None
branch_labels = None
depends_on = None
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
tables = inspector.get_table_names()
def upgrade():
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
tables = inspector.get_table_names()
if "app" not in tables:
op.create_table(
"app",
......@@ -69,4 +69,4 @@ def downgrade():
op.drop_table("oauthclient_app")
op.drop_table("app_role")
op.drop_table("role")
op.drop_table("app")
\ No newline at end of file
op.drop_table("app")
"""Add SCIM support for user provisioning
Revision ID: 825262488cd9
Revises: fdb28e81f5c2
Create Date: 2023-03-08 10:50:00
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
from sqlalchemy.engine.reflection import Inspector
from helpers.provision import ProvisionStatus
# revision identifiers, used by Alembic.
revision = '825262488cd9'
down_revision = 'fdb28e81f5c2'
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"app_role",
sa.Column(
"provision_status",
sa.Enum(
ProvisionStatus,
native_enum=False,
length=32,
values_callable=lambda _: [str(member.value) for member in ProvisionStatus]
),
nullable=False,
default=ProvisionStatus.SyncNeeded,
server_default=ProvisionStatus.SyncNeeded.value
),
)
op.add_column(
"app_role",
sa.Column(
"last_provision_attempt",
sa.DateTime,
nullable=True
),
)
op.add_column(
"app_role",
sa.Column(
"last_provision_message",
sa.Unicode(length=256),
nullable=True
),
)
def downgrade():
op.drop_column("app_role", "provision_status")
op.drop_column("app_role", "last_provision_attempt")
op.drop_column("app_role", "last_provision_message")
"""Extend SCIM support to include some attributes during provisioning only when
they are changed, or the user is first created in the app.
Revision ID: 9ee5a7d65fa7
Revises: 267d280db490
Create Date: 2024-06-04 15:39:00
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9ee5a7d65fa7'
down_revision = '267d280db490'
branch_labels = None
depends_on = None
def upgrade():
# An entry in this table records that a certain user attribute needs to be
# set in a certain app via SCIM.
op.create_table(
"scim_attribute",
sa.Column("user_id", sa.String(length=64), nullable=False),
sa.Column("app_id", sa.Integer(), nullable=False),
sa.Column("attribute", sa.String(length=64), nullable=False),
sa.PrimaryKeyConstraint("user_id", "app_id", "attribute"),
sa.ForeignKeyConstraint(["app_id"],["app.id"]),
)
def downgrade():
op.drop_table("scim_attribute")
"""Add tags for user management.
Revision ID: fdb28e81f5c2
Revises: 7d27395c892a
Create Date: 2023-11-21 14:55:00
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
from sqlalchemy.engine.reflection import Inspector
# revision identifiers, used by Alembic.
revision = 'fdb28e81f5c2'
down_revision = '7d27395c892a'
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"tag",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=256), nullable=False),
sa.Column("colour", sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"tag_user",
sa.Column("user_id", sa.String(length=64), nullable=False),
sa.Column("tag_id", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint("user_id", "tag_id"),
sa.ForeignKeyConstraint(["tag_id"],["tag.id"]),
)
def downgrade():
op.drop_table("tag_user")
op.drop_table("tag")
APScheduler==3.11.0
# CLI creation kit
click==8.1.8
cryptography==44.0.2
Flask==3.1.0
Flask-Cors==5.0.1
flask-expects-json==1.7.0
Flask-JWT-Extended==4.7.1
Flask-Migrate==4.1.0
Flask-SQLAlchemy==3.1.1
gunicorn==23.0.0
jsonschema==4.23.0
# Templating kustomizations as part of app installation.
jinja2-base64-filters==0.1.4
kubernetes==32.0.1
pymysql==1.1.1
NamedAtomicLock==1.1.3
ory-kratos-client==1.3.8
ory-hydra-client==2.2.0
pip-install==1.3.5
posix-ipc==1.1.1
PyYAML==6.0.2
regex==2024.11.6
requests==2.32.3
requests-oauthlib==2.0.0
attrs==21.4.0
black==22.1.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.12
click==8.0.4
cryptography==36.0.2
Flask==2.0.3
Flask-Cors==3.0.10
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --no-emit-index-url --output-file=requirements.txt --strip-extras requirements.in
#
alembic==1.15.1
# via flask-migrate
annotated-types==0.7.0
# via pydantic
apscheduler==3.11.0
# via -r requirements.in
attrs==25.3.0
# via
# jsonschema
# referencing
blinker==1.9.0
# via flask
cachetools==5.5.2
# via google-auth
certifi==2025.1.31
# via
# kubernetes
# requests
cffi==1.17.1
# via cryptography
charset-normalizer==3.4.1
# via requests
click==8.1.8
# via
# -r requirements.in
# flask
cryptography==44.0.2
# via -r requirements.in
durationpy==0.9
# via kubernetes
flask==3.1.0
# via
# -r requirements.in
# flask-cors
# flask-expects-json
# flask-jwt-extended
# flask-migrate
# flask-sqlalchemy
flask-cors==5.0.1
# via -r requirements.in
flask-expects-json==1.7.0
Flask-JWT-Extended==4.3.1
Flask-Migrate==4.0.1
Flask-SQLAlchemy==2.5.1
gunicorn==20.1.0
idna==3.3
install==1.3.5
itsdangerous==2.1.1
jsonschema==4.4.0
Jinja2==3.0.3
# via -r requirements.in
flask-jwt-extended==4.7.1
# via -r requirements.in
flask-migrate==4.1.0
# via -r requirements.in
flask-sqlalchemy==3.1.1
# via
# -r requirements.in
# flask-migrate
google-auth==2.38.0
# via kubernetes
greenlet==3.1.1
# via sqlalchemy
gunicorn==23.0.0
# via -r requirements.in
idna==3.10
# via requests
itsdangerous==2.2.0
# via flask
jinja2==3.1.6
# via
# flask
# jinja2-base64-filters
jinja2-base64-filters==0.1.4
kubernetes==24.2.0
MarkupSafe==2.1.1
mypy-extensions==0.4.3
NamedAtomicLock==1.1.3
oauthlib==3.2.0
ory-kratos-client==0.11.0
ory-hydra-client==1.11.8
pathspec==0.9.0
platformdirs==2.5.1
pycparser==2.21
PyJWT==2.3.0
pymysql==1.0.2
pyrsistent==0.18.1
PyYAML==6.0
regex==2022.3.15
requests==2.27.1
requests-oauthlib==1.3.1
six==1.16.0
tomli==1.2.3
typing-extensions==4.1.1
urllib3==1.26.8
Werkzeug==2.0.3
# via -r requirements.in
jsonschema==4.23.0
# via
# -r requirements.in
# flask-expects-json
jsonschema-specifications==2024.10.1
# via jsonschema
kubernetes==32.0.1
# via -r requirements.in
mako==1.3.9
# via alembic
markupsafe==3.0.2
# via
# jinja2
# mako
# werkzeug
namedatomiclock==1.1.3
# via -r requirements.in
oauthlib==3.2.2
# via
# kubernetes
# requests-oauthlib
ory-hydra-client==2.2.0
# via -r requirements.in
ory-kratos-client==1.3.8
# via -r requirements.in
packaging==24.2
# via gunicorn
pip-install==1.3.5
# via -r requirements.in
posix-ipc==1.1.1
# via -r requirements.in
pyasn1==0.6.1
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.4.1
# via google-auth
pycparser==2.22
# via cffi
pydantic==2.10.6
# via ory-kratos-client
pydantic-core==2.27.2
# via pydantic
pyjwt==2.10.1
# via flask-jwt-extended
pymysql==1.1.1
# via -r requirements.in
python-dateutil==2.9.0.post0
# via
# kubernetes
# ory-hydra-client
# ory-kratos-client
pyyaml==6.0.2
# via
# -r requirements.in
# kubernetes
referencing==0.36.2
# via
# jsonschema
# jsonschema-specifications
regex==2024.11.6
# via -r requirements.in
requests==2.32.3
# via
# -r requirements.in
# kubernetes
# requests-oauthlib
requests-oauthlib==2.0.0
# via
# -r requirements.in
# kubernetes
rpds-py==0.23.1
# via
# jsonschema
# referencing
rsa==4.9
# via google-auth
six==1.17.0
# via
# kubernetes
# python-dateutil
sqlalchemy==2.0.39
# via
# alembic
# flask-sqlalchemy
typing-extensions==4.12.2
# via
# alembic
# ory-kratos-client
# pydantic
# pydantic-core
# referencing
# sqlalchemy
tzlocal==5.3.1
# via apscheduler
urllib3==2.3.0
# via
# kubernetes
# ory-hydra-client
# ory-kratos-client
# requests
websocket-client==1.8.0
# via kubernetes
werkzeug==3.1.3
# via
# flask
# flask-cors
# flask-jwt-extended
This `web` directory is responsible for authentication frontend components.
It uses Tailwind for CSS; when making UI changes open a terminal in the `web` directory and run
`npx tailwindcss -i ./static/src/input.css -o ./static/css/main.css --watch`
This diff is collapsed.
......@@ -21,7 +21,7 @@
var dashboard_url = "";
// Render a message by appending the data to the messages box. The message id is
// availble, potentially for future translations/locale handling
// available, potentially for future translations/locale handling
// @param string id Message ID\
// @param string message Message in the default language (English)
// @param string type Type of message, currently only "error" renders in
......@@ -57,7 +57,7 @@ function check_flow_auth() {
// Set a custom cookie so the settings page knows we're in
// recovery context and can open the right tab.
Cookies.set("stackspin_context", "recovery");
window.location.href = api_url + '/self-service/settings/browser';
window.location.href = api_url + "/self-service/settings/browser";
return;
}
......@@ -93,11 +93,22 @@ function flow_login() {
url: uri,
success: function (data) {
// Determine which groups to show.
window.console.log("flow_login: selecting UI groups to process");
var groups = scrape_groups(data);
for (const group of groups) {
// Render login form (group: password)
window.console.log("flow_login: processing group " + group);
var form_html = render_form(data, group, "login");
if (group == "oidc" && groups.has("password")) {
// Show a separator between password login and oidc login.
$("#separator_oidc").removeClass("hide");
}
$("#contentLogin_" + group).html(form_html);
// Hide the recovery link on the 2FA entry
// forms. It's not really useful at that point, and
// you may get a redirect loop if you use it.
if (group == 'totp' || group == 'webauthn') {
$("#recoveryLink").hide();
}
}
render_messages(data);
......@@ -150,8 +161,8 @@ function flow_settings_validate() {
}
// Render the settings flow, this is where users can change their personal
// settings, like name, password and totp (second factor). The form contents
// are defined by Kratos.
// settings, like name, password and second factors (2FA, WebAuthn). The form
// contents are defined by Kratos.
function flow_settings() {
// Get the details from the current flow from kratos
var flow = $.urlParam("flow");
......@@ -178,12 +189,13 @@ function flow_settings() {
if (state == "recovery") {
$("#contentProfile").hide();
$("#contentTotp").hide();
$("#contentWebAuthn").hide();
}
// Render messages given from the API
render_messages(data);
// Render the forms (password, profile, totp) based on the fields we got
// Render the settings forms based on the fields we got
// from the API.
var html = render_form(data, "password", "settings");
$("#pills-password").html(html);
......@@ -194,6 +206,9 @@ function flow_settings() {
html = render_form(data, "totp", "settings");
$("#pills-totp").html(html);
html = render_form(data, "webauthn", "settings");
$("#pills-webauthn").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.
......@@ -215,7 +230,17 @@ function flow_settings() {
// If we are in recovery context, switch to the password tab.
if (context == "recovery") {
$('#pills-password-tab').tab('show');
$("#pills-password-tab").tab("show");
Cookies.set("stackspin_context", "");
}
// If the user is required to set up 2FA, switch to
// that tab and show a message.
if (context == "2fa-required") {
$('#pills-totp-tab').tab('show');
$("#contentMessages").html('Setting up a second factor is required to continue.');
$("#contentMessages").addClass("alert");
$("#contentMessages").addClass("alert-warning");
Cookies.set('stackspin_context', '');
}
},
......@@ -235,7 +260,7 @@ function flow_settings() {
// Redirect so user can enter 2FA.
window.location.href = response.redirect_browser_to;
return;
}
}
}
// There was another error, one we don't specifically prepared for.
$("#contentProfileSaveFailed").show();
......@@ -328,6 +353,12 @@ function render_form(data, group, context) {
for (const node of data.ui.nodes) {
if (node.group == "default" || node.group == group) {
// We do not want to show the identifier (email
// address) input *again* in the OIDC form, which is only a
// single "Sign in with ..." button.
if (group == "oidc" && node.attributes.name == "identifier") {
continue;
}
var elm = getFormElement(node, context);
form += elm;
}
......@@ -343,28 +374,18 @@ function render_messages(data) {
return "";
}
// Not show again if already shown. User probably just reloaded the screen
if (Cookies.get("last_flow_id_rendered_message") == data.id) {
return;
}
var html = "";
messages.forEach((message) => {
var type = message.type;
if (type == "error") {
type = "danger";
}
html += "<div class=\"alert alert-" + type + "\">";
html += message.text;
html += "</br>";
html += "</div>";
});
$("#contentMessages").html(html);
$("#contentMessages").addClass("alert");
if (data.state == "success") {
$("#contentMessages").addClass("alert-success");
} else {
$("#contentMessages").addClass("alert-warning");
}
// Store we shown these messages
Cookies.set("last_flow_id_rendered_message", data.id);
return html;
}
......@@ -372,15 +393,23 @@ function render_messages(data) {
// 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
// labels.
// type: input type, usual "input", "hidden" or "submit". But bootstrap types
// like "email" are also supported
// like "email" are also supported.
// name: name of the field. Used when posting data
// value: If there is already a value known, show it
// messages: error messages related to the field
function getFormElement(node, context) {
console.log("Getting form element", node);
if (node.attributes.node_type == "script") {
return (
`<script src="` +
node.attributes.src +
`" defer>`
);
}
if (node.type == "img") {
return (
`
......@@ -428,6 +457,11 @@ function getFormElement(node, context) {
readonly = false;
label =
"Please provide your e-mail address. If it is registered, we will send a recovery link to that address.";
var email = Cookies.get("recovery_preset_email");
if (email) {
value = email;
Cookies.set("recovery_preset_email", "");
}
}
return getFormInput(
"email",
......@@ -465,18 +499,6 @@ function getFormElement(node, context) {
);
}
if (name == "identifier") {
return getFormInput(
"email",
name,
value,
"E-mail address",
"Please provide your e-mail address to log in",
null,
messages
);
}
if (name == "password") {
return getFormInput(
"password",
......@@ -503,6 +525,18 @@ function getFormElement(node, context) {
);
}
if (name == "identifier") {
return getFormInput(
"email",
name,
value,
"E-mail address",
"Please provide your e-mail address to log in",
null,
messages
);
}
if (name == "totp_code") {
return getFormInput(
"totp_code",
......@@ -511,12 +545,30 @@ function getFormElement(node, context) {
"TOTP code",
"Please enter the code from your TOTP/authenticator app.",
null,
messages
messages,
null,
false
);
}
if (type == "button") {
var label = node.meta.label.text || "Unknown";
// if (name == "webauthn_login_trigger") {
// label = "Confirm with WebAuthn";
// }
const oc = node.attributes.onclick;
return (
`<div class="form-group flex justify-end py-2">
<button type="button" name="` + name + `" onclick='` + oc + `' class="inline-flex h-10 items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">` +
label +
`</button>
</div>`
);
}
if (type == "submit") {
var label = "Save";
var justify = "justify-end";
if (name == "totp_unlink") {
label = "Forget saved TOTP device";
} else if (node.group == "totp") {
......@@ -536,8 +588,15 @@ function getFormElement(node, context) {
if (context == "recovery") {
label = "Send recovery link";
}
if (node.group == "oidc") {
label = node.meta.label.text;
justify = "justify-center";
}
if (name == "webauthn_remove") {
label = node.meta.label.text;
}
return (
`<div class="form-group flex justify-end py-2">
`<div class="form-group flex ` + justify + ` py-2">
<input type="hidden" name="` +
name +
`" value="` +
......@@ -550,7 +609,7 @@ function getFormElement(node, context) {
);
}
return getFormInput("input", name, value, name, null, null, messages);
return getFormInput("input", name, value, node.meta.label.text || name, null, null, messages);
}
// Usually called by getFormElement, generic function to generate an
......@@ -563,6 +622,8 @@ function getFormElement(node, context) {
// param help: Additional help text, displayed below the field in small font
// param messages: Message about failed input
// param readonly: Whether the input should be readonly (defaults to false)
// param autocomplete: Whether the input should be autocompleted by the browser
// (defaults to true)
function getFormInput(
type,
name,
......@@ -571,7 +632,8 @@ function getFormInput(
placeHolder,
help,
messages,
readonly
readonly,
autocomplete
) {
if (typeof help == "undefined" || help == null) {
help = "";
......@@ -579,6 +641,9 @@ function getFormInput(
if (typeof readonly == "undefined") {
readonly = false;
}
if (typeof autocomplete == "undefined") {
autocomplete = true;
}
console.log("Messages: ", messages);
// Id field for help element
......@@ -616,6 +681,9 @@ function getFormInput(
if (readonly) {
element += "readonly ";
}
if (! autocomplete) {
element += "autocomplete=\"off\" ";
}
element += ">";
if (help) {
......@@ -643,3 +711,12 @@ $.urlParam = function (name) {
}
return decodeURI(results[1]) || 0;
};
window.alert = function(message) {
window.console.log("Alert: " + message);
var alertType = "info";
if ((new RegExp("error", "i")).test(message)) {
alertType = "error";
}
renderMessage('', message, alertType);
};
This diff is collapsed.
backend/web/static/favicon.ico

14.7 KiB

//////////////////////////////////////
// sign up form for the demo instance
//////////////////////////////////////
function submitSignup() {
let result = document.querySelector("#signup-result");
let email = document.querySelector("#signup-email");
let xhr = new XMLHttpRequest();
xhr.responseType = "json";
let url = "/web/demo-user";
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
// In the success case, we get a plain (json) string; in the error
// case, we get an object with `errorMessage` property.
if (typeof this.response == "object" && "errorMessage" in this.response) {
window.console.log("Error in sign-up request.");
result.classList.remove("alert-success");
result.classList.add("alert-danger");
if (
this.response.errorMessage ==
"[KratosError] Unable to insert or update resource because a resource with that value exists already"
) {
result.innerHTML = "A user with that email address already exists.";
} else if (
this.response.errorMessage ==
"[KratosError] The request was malformed or contained invalid parameters"
) {
result.innerHTML = "That doesn't appear to be a valid email address.";
} else {
result.innerHTML = this.response.errorMessage;
}
} else {
result.classList.add("alert-success");
result.classList.remove("alert-danger");
result.innerHTML = this.response;
}
result.style.visibility = "visible";
}
};
// Converting JSON data to string
var data = JSON.stringify({ email: email.value });
// Sending data with the request
xhr.send(data);
}
......@@ -2,6 +2,12 @@
@tailwind components;
@tailwind utilities;
@layer base {
label {
@apply block text-sm font-medium text-gray-700 mb-1;
}
}
@layer components {
#pills-tab .nav-link.active {
@apply bg-transparent text-gray-800 border-b-2 text-left border-b-primary-700 hover:bg-primary-700 py-2 px-4 rounded-none;
......
......@@ -30,3 +30,24 @@ button {
#totp_qr {
padding: 2rem;
}
.hide {
display: none !important;
}
.separator {
display: flex;
align-items: center;
text-align: center;
}
.separator::before, .separator::after {
content: '';
flex: 1;
border-bottom: 0.15rem solid #1e8290;
}
.separator:not(:empty)::before {
margin-right: .4em;
}
.separator:not(:empty)::after {
margin-left: .4em;
}
/** @type {import('tailwindcss').Config} */
module.exports = {
mode: "jit",
content: ["./templates/**/*.{html,htm}", "./static/base.js"],
theme: {
extend: {
colors: {
primary: {
50: "#F2FFFF",
100: "#D6FDFF",
200: "#B6F7FB",
300: "#7AE5EA",
400: "#55C6CC",
500: "#39A9B1",
600: "#24929C",
700: "#157983",
800: "#135D66",
900: "#0F4F57",
light: "#54C6CC",
DEFAULT: "#54C6CC",
dark: "#1E8290",
},
},
},
},
plugins: [],
mode: "jit",
content: ["./templates/**/*.{html,htm}", "./static/base.js"],
theme: {
extend: {
colors: {
primary: {
50: "var(--colour-primary-50)",
100: "var(--colour-primary-100)",
200: "var(--colour-primary-200)",
300: "var(--colour-primary-300)",
400: "var(--colour-primary-400)",
500: "var(--colour-primary-500)",
600: "var(--colour-primary-600)",
700: "var(--colour-primary-700)",
800: "var(--colour-primary-800)",
900: "var(--colour-primary-900)",
950: "var(--colour-primary-950)",
light: "var(--colour-primary-light)",
DEFAULT: "var(--colour-primary-default)",
dark: "var(--colour-primary-dark)",
},
},
},
},
plugins: [],
};
<!doctype html>
<!DOCTYPE html>
<html class="h-full bg-white">
<link rel="stylesheet" href="static/style.css">
<link rel="stylesheet" href="static/css/bootstrap.min.css">
<link href="static/css/main.css" rel="stylesheet">
<script src="static/js/jquery-3.6.0.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/js.cookie.min.js"></script>
<script src="static/base.js"></script>
<title>Your Stackspin Account</title>
</html>
<body class="h-full bg-gray-50">
<head>
<style>
:root {
--colour-primary-50: #F2FFFF;
--colour-primary-100: #D6FDFF;
--colour-primary-200: #B6F7FB;
--colour-primary-300: #7AE5EA;
--colour-primary-400: #55C6CC;
--colour-primary-500: #39A9B1;
--colour-primary-600: #24929C;
--colour-primary-700: #157983;
--colour-primary-800: #135D66;
--colour-primary-900: #0F4F57;
--colour-primary-950: #0A353A;
--colour-primary-light: #54C6CC;
--colour-primary-default: #54C6CC;
--colour-primary-dark: #1E8290;
}
</style>
<link rel="stylesheet" href="../style.css" />
<link rel="stylesheet" href="static/style.css" />
<link rel="icon" type="image/x-icon" href="/static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link href="static/css/main.css" rel="stylesheet" />
<script src="static/js/jquery-3.6.0.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/js.cookie.min.js"></script>
<script src="static/base.js"></script>
{% if demo %}
<script src="static/js/demo.js"></script>
{% endif %}
<title>Your Stackspin Account</title>
</head>
<body class="stackspin-background h-full bg-gray-50">
<script>
var api_url = '{{ api_url }}';
// Actions
$(document).ready(function () {
check_flow_expired();
});
var api_url = "{{ api_url }}";
// Actions
$(document).ready(function () {
check_flow_expired();
});
</script>
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="mx-auto w-full h-full md:w-2/3 md:max-w-lg bg-white shadow rounded-lg space-y-8 mb-4 md:mb-0 p-8">
<div id="contentFlowExpired" class='alert alert-warning' style='display:none'>Your request has expired.
Please resubmit your request.</div>
<a href="{{ dashboard_url }}"><img class="mx-auto h-10 w-auto mb-4" src='static/logo.svg' /></a>
<!-- <h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Your Stackspin
account</h2> -->
<div id='messages'></div>
{% block content %}{% endblock %}
</div>
</div>
\ No newline at end of file
<div
class="mx-auto w-full h-full md:w-2/3 md:max-w-xl bg-white shadow rounded-lg space-y-8 mb-4 md:mb-0 p-8 pt-0"
>
<div
id="contentFlowExpired"
class="alert alert-warning"
style="display: none"
>
Your request has expired. Please resubmit your request.
</div>
<div class="login-header flex flex-col place-items-center">
<a href="{{ dashboard_url }}" class="block">
<img class="stackspin-logo mx-auto h-10 w-auto" src="static/logo.svg" />
</a>
{% if demo %}
<span
class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-500 ring-1 ring-inset ring-gray-300"
>
Demo Instance
</span>
{% endif %}
</div>
<div id="messages"></div>
{% block content %}{% endblock %}
</div>
</div>
</body>
</html>
......@@ -6,9 +6,9 @@
// Wipe the local storage
localStorage.removeItem("persist:root");
// Redirect
window.location = '{{ url }}';
window.location = '{{ url | safe }}';
</script>
Redirecting ...
Clearing session data and redirecting...
{% endblock %}
This diff is collapsed.
......@@ -23,17 +23,21 @@
<a class="nav-link" id="pills-password-tab" data-toggle="pill" href="#pills-password" role="tab"
aria-controls="pills-password" aria-selected="false">Change password</a>
<a class="nav-link" id="pills-totp-tab" data-toggle="pill" href="#pills-totp" role="tab" aria-controls="pills-totp"
aria-selected="false">2nd factor authentication</a>
aria-selected="false">2nd factor: TOTP</a>
<a class="nav-link" id="pills-webauthn-tab" data-toggle="pill" href="#pills-webauthn" role="tab" aria-controls="pills-webauthn"
aria-selected="false">2nd factor: WebAuthn</a>
</div>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade show active" id="pills-profile" role="tabpanel" aria-labelledby="pills-profile-tab">...
</div>
<div class="tab-pane fade" id="pills-password" role="tabpanel" aria-labelledby="pills-password-tab">...</div>
<div class="tab-pane fade" id="pills-totp" role="tabpanel" aria-labelledby="pills-totp-tab">...</div>
<div class="tab-pane fade" id="pills-webauthn" role="tabpanel" aria-labelledby="pills-webauthn-tab">...</div>
</div>
<div id="contentProfile"></div>
<div id="contentPassword"></div>
<div id="contentTotp"></div>
<div id="contentWebauthn"></div>
{% endblock %}
\ No newline at end of file
{% endblock %}