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 ...@@ -16,11 +16,11 @@ down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
tables = inspector.get_table_names()
def upgrade(): def upgrade():
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
tables = inspector.get_table_names()
if "app" not in tables: if "app" not in tables:
op.create_table( op.create_table(
"app", "app",
...@@ -69,4 +69,4 @@ def downgrade(): ...@@ -69,4 +69,4 @@ def downgrade():
op.drop_table("oauthclient_app") op.drop_table("oauthclient_app")
op.drop_table("app_role") op.drop_table("app_role")
op.drop_table("role") op.drop_table("role")
op.drop_table("app") op.drop_table("app")
\ No newline at end of file
"""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 # This file is autogenerated by pip-compile with Python 3.12
certifi==2021.10.8 # by the following command:
cffi==1.15.0 #
charset-normalizer==2.0.12 # pip-compile --no-emit-index-url --output-file=requirements.txt --strip-extras requirements.in
click==8.0.4 #
cryptography==36.0.2 alembic==1.15.1
Flask==2.0.3 # via flask-migrate
Flask-Cors==3.0.10 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-expects-json==1.7.0
Flask-JWT-Extended==4.3.1 # via -r requirements.in
Flask-Migrate==4.0.1 flask-jwt-extended==4.7.1
Flask-SQLAlchemy==2.5.1 # via -r requirements.in
gunicorn==20.1.0 flask-migrate==4.1.0
idna==3.3 # via -r requirements.in
install==1.3.5 flask-sqlalchemy==3.1.1
itsdangerous==2.1.1 # via
jsonschema==4.4.0 # -r requirements.in
Jinja2==3.0.3 # 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 jinja2-base64-filters==0.1.4
kubernetes==24.2.0 # via -r requirements.in
MarkupSafe==2.1.1 jsonschema==4.23.0
mypy-extensions==0.4.3 # via
NamedAtomicLock==1.1.3 # -r requirements.in
oauthlib==3.2.0 # flask-expects-json
ory-kratos-client==0.11.0 jsonschema-specifications==2024.10.1
ory-hydra-client==1.11.8 # via jsonschema
pathspec==0.9.0 kubernetes==32.0.1
platformdirs==2.5.1 # via -r requirements.in
pycparser==2.21 mako==1.3.9
PyJWT==2.3.0 # via alembic
pymysql==1.0.2 markupsafe==3.0.2
pyrsistent==0.18.1 # via
PyYAML==6.0 # jinja2
regex==2022.3.15 # mako
requests==2.27.1 # werkzeug
requests-oauthlib==1.3.1 namedatomiclock==1.1.3
six==1.16.0 # via -r requirements.in
tomli==1.2.3 oauthlib==3.2.2
typing-extensions==4.1.1 # via
urllib3==1.26.8 # kubernetes
Werkzeug==2.0.3 # 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 @@ ...@@ -21,7 +21,7 @@
var dashboard_url = ""; var dashboard_url = "";
// Render a message by appending the data to the messages box. The message id is // 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 id Message ID\
// @param string message Message in the default language (English) // @param string message Message in the default language (English)
// @param string type Type of message, currently only "error" renders in // @param string type Type of message, currently only "error" renders in
...@@ -57,7 +57,7 @@ function check_flow_auth() { ...@@ -57,7 +57,7 @@ function check_flow_auth() {
// Set a custom cookie so the settings page knows we're in // Set a custom cookie so the settings page knows we're in
// recovery context and can open the right tab. // recovery context and can open the right tab.
Cookies.set("stackspin_context", "recovery"); Cookies.set("stackspin_context", "recovery");
window.location.href = api_url + '/self-service/settings/browser'; window.location.href = api_url + "/self-service/settings/browser";
return; return;
} }
...@@ -93,11 +93,22 @@ function flow_login() { ...@@ -93,11 +93,22 @@ function flow_login() {
url: uri, url: uri,
success: function (data) { success: function (data) {
// Determine which groups to show. // Determine which groups to show.
window.console.log("flow_login: selecting UI groups to process");
var groups = scrape_groups(data); var groups = scrape_groups(data);
for (const group of groups) { 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"); 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); $("#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); render_messages(data);
...@@ -150,8 +161,8 @@ function flow_settings_validate() { ...@@ -150,8 +161,8 @@ function flow_settings_validate() {
} }
// Render the settings flow, this is where users can change their personal // Render the settings flow, this is where users can change their personal
// settings, like name, password and totp (second factor). The form contents // settings, like name, password and second factors (2FA, WebAuthn). The form
// are defined by Kratos. // contents are defined by Kratos.
function flow_settings() { function flow_settings() {
// Get the details from the current flow from kratos // Get the details from the current flow from kratos
var flow = $.urlParam("flow"); var flow = $.urlParam("flow");
...@@ -178,12 +189,13 @@ function flow_settings() { ...@@ -178,12 +189,13 @@ function flow_settings() {
if (state == "recovery") { if (state == "recovery") {
$("#contentProfile").hide(); $("#contentProfile").hide();
$("#contentTotp").hide(); $("#contentTotp").hide();
$("#contentWebAuthn").hide();
} }
// Render messages given from the API // Render messages given from the API
render_messages(data); 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. // from the API.
var html = render_form(data, "password", "settings"); var html = render_form(data, "password", "settings");
$("#pills-password").html(html); $("#pills-password").html(html);
...@@ -194,6 +206,9 @@ function flow_settings() { ...@@ -194,6 +206,9 @@ function flow_settings() {
html = render_form(data, "totp", "settings"); html = render_form(data, "totp", "settings");
$("#pills-totp").html(html); $("#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. // If the submit button is hit, execute the POST with Ajax.
$("#formpassword").submit(function (e) { $("#formpassword").submit(function (e) {
// avoid to execute the actual submit of the form. // avoid to execute the actual submit of the form.
...@@ -215,7 +230,17 @@ function flow_settings() { ...@@ -215,7 +230,17 @@ function flow_settings() {
// If we are in recovery context, switch to the password tab. // If we are in recovery context, switch to the password tab.
if (context == "recovery") { 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', ''); Cookies.set('stackspin_context', '');
} }
}, },
...@@ -235,7 +260,7 @@ function flow_settings() { ...@@ -235,7 +260,7 @@ function flow_settings() {
// Redirect so user can enter 2FA. // Redirect so user can enter 2FA.
window.location.href = response.redirect_browser_to; window.location.href = response.redirect_browser_to;
return; return;
} }
} }
// There was another error, one we don't specifically prepared for. // There was another error, one we don't specifically prepared for.
$("#contentProfileSaveFailed").show(); $("#contentProfileSaveFailed").show();
...@@ -328,6 +353,12 @@ function render_form(data, group, context) { ...@@ -328,6 +353,12 @@ function render_form(data, group, context) {
for (const node of data.ui.nodes) { for (const node of data.ui.nodes) {
if (node.group == "default" || node.group == group) { 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); var elm = getFormElement(node, context);
form += elm; form += elm;
} }
...@@ -343,28 +374,18 @@ function render_messages(data) { ...@@ -343,28 +374,18 @@ function render_messages(data) {
return ""; 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 = ""; var html = "";
messages.forEach((message) => { messages.forEach((message) => {
var type = message.type;
if (type == "error") {
type = "danger";
}
html += "<div class=\"alert alert-" + type + "\">";
html += message.text; html += message.text;
html += "</br>"; html += "</div>";
}); });
$("#contentMessages").html(html); $("#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; return html;
} }
...@@ -372,15 +393,23 @@ function render_messages(data) { ...@@ -372,15 +393,23 @@ function render_messages(data) {
// Kratos give us form names and types and specifies what to render. However // 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 // 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 // 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 // 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 // name: name of the field. Used when posting data
// value: If there is already a value known, show it // value: If there is already a value known, show it
// messages: error messages related to the field // messages: error messages related to the field
function getFormElement(node, context) { function getFormElement(node, context) {
console.log("Getting form element", node); console.log("Getting form element", node);
if (node.attributes.node_type == "script") {
return (
`<script src="` +
node.attributes.src +
`" defer>`
);
}
if (node.type == "img") { if (node.type == "img") {
return ( return (
` `
...@@ -428,6 +457,11 @@ function getFormElement(node, context) { ...@@ -428,6 +457,11 @@ function getFormElement(node, context) {
readonly = false; readonly = false;
label = label =
"Please provide your e-mail address. If it is registered, we will send a recovery link to that address."; "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( return getFormInput(
"email", "email",
...@@ -465,18 +499,6 @@ function getFormElement(node, context) { ...@@ -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") { if (name == "password") {
return getFormInput( return getFormInput(
"password", "password",
...@@ -503,6 +525,18 @@ function getFormElement(node, context) { ...@@ -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") { if (name == "totp_code") {
return getFormInput( return getFormInput(
"totp_code", "totp_code",
...@@ -511,12 +545,30 @@ function getFormElement(node, context) { ...@@ -511,12 +545,30 @@ function getFormElement(node, context) {
"TOTP code", "TOTP code",
"Please enter the code from your TOTP/authenticator app.", "Please enter the code from your TOTP/authenticator app.",
null, 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") { if (type == "submit") {
var label = "Save"; var label = "Save";
var justify = "justify-end";
if (name == "totp_unlink") { if (name == "totp_unlink") {
label = "Forget saved TOTP device"; label = "Forget saved TOTP device";
} else if (node.group == "totp") { } else if (node.group == "totp") {
...@@ -536,8 +588,15 @@ function getFormElement(node, context) { ...@@ -536,8 +588,15 @@ function getFormElement(node, context) {
if (context == "recovery") { if (context == "recovery") {
label = "Send recovery link"; 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 ( return (
`<div class="form-group flex justify-end py-2"> `<div class="form-group flex ` + justify + ` py-2">
<input type="hidden" name="` + <input type="hidden" name="` +
name + name +
`" value="` + `" value="` +
...@@ -550,7 +609,7 @@ function getFormElement(node, context) { ...@@ -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 // Usually called by getFormElement, generic function to generate an
...@@ -563,6 +622,8 @@ function getFormElement(node, context) { ...@@ -563,6 +622,8 @@ function getFormElement(node, context) {
// param help: Additional help text, displayed below the field in small font // param help: Additional help text, displayed below the field in small font
// param messages: Message about failed input // param messages: Message about failed input
// param readonly: Whether the input should be readonly (defaults to false) // 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( function getFormInput(
type, type,
name, name,
...@@ -571,7 +632,8 @@ function getFormInput( ...@@ -571,7 +632,8 @@ function getFormInput(
placeHolder, placeHolder,
help, help,
messages, messages,
readonly readonly,
autocomplete
) { ) {
if (typeof help == "undefined" || help == null) { if (typeof help == "undefined" || help == null) {
help = ""; help = "";
...@@ -579,6 +641,9 @@ function getFormInput( ...@@ -579,6 +641,9 @@ function getFormInput(
if (typeof readonly == "undefined") { if (typeof readonly == "undefined") {
readonly = false; readonly = false;
} }
if (typeof autocomplete == "undefined") {
autocomplete = true;
}
console.log("Messages: ", messages); console.log("Messages: ", messages);
// Id field for help element // Id field for help element
...@@ -616,6 +681,9 @@ function getFormInput( ...@@ -616,6 +681,9 @@ function getFormInput(
if (readonly) { if (readonly) {
element += "readonly "; element += "readonly ";
} }
if (! autocomplete) {
element += "autocomplete=\"off\" ";
}
element += ">"; element += ">";
if (help) { if (help) {
...@@ -643,3 +711,12 @@ $.urlParam = function (name) { ...@@ -643,3 +711,12 @@ $.urlParam = function (name) {
} }
return decodeURI(results[1]) || 0; 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 @@ ...@@ -2,6 +2,12 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
label {
@apply block text-sm font-medium text-gray-700 mb-1;
}
}
@layer components { @layer components {
#pills-tab .nav-link.active { #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; @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 { ...@@ -30,3 +30,24 @@ button {
#totp_qr { #totp_qr {
padding: 2rem; 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} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
mode: "jit", mode: "jit",
content: ["./templates/**/*.{html,htm}", "./static/base.js"], content: ["./templates/**/*.{html,htm}", "./static/base.js"],
theme: { theme: {
extend: { extend: {
colors: { colors: {
primary: { primary: {
50: "#F2FFFF", 50: "var(--colour-primary-50)",
100: "#D6FDFF", 100: "var(--colour-primary-100)",
200: "#B6F7FB", 200: "var(--colour-primary-200)",
300: "#7AE5EA", 300: "var(--colour-primary-300)",
400: "#55C6CC", 400: "var(--colour-primary-400)",
500: "#39A9B1", 500: "var(--colour-primary-500)",
600: "#24929C", 600: "var(--colour-primary-600)",
700: "#157983", 700: "var(--colour-primary-700)",
800: "#135D66", 800: "var(--colour-primary-800)",
900: "#0F4F57", 900: "var(--colour-primary-900)",
light: "#54C6CC", 950: "var(--colour-primary-950)",
DEFAULT: "#54C6CC", light: "var(--colour-primary-light)",
dark: "#1E8290", DEFAULT: "var(--colour-primary-default)",
}, dark: "var(--colour-primary-dark)",
}, },
}, },
}, },
plugins: [], },
plugins: [],
}; };
<!doctype html> <!DOCTYPE html>
<html class="h-full bg-white"> <html class="h-full bg-white">
<link rel="stylesheet" href="static/style.css"> <head>
<style>
<link rel="stylesheet" href="static/css/bootstrap.min.css"> :root {
<link href="static/css/main.css" rel="stylesheet"> --colour-primary-50: #F2FFFF;
<script src="static/js/jquery-3.6.0.min.js"></script> --colour-primary-100: #D6FDFF;
<script src="static/js/bootstrap.bundle.min.js"></script> --colour-primary-200: #B6F7FB;
<script src="static/js/js.cookie.min.js"></script> --colour-primary-300: #7AE5EA;
<script src="static/base.js"></script> --colour-primary-400: #55C6CC;
--colour-primary-500: #39A9B1;
<title>Your Stackspin Account</title> --colour-primary-600: #24929C;
--colour-primary-700: #157983;
</html> --colour-primary-800: #135D66;
--colour-primary-900: #0F4F57;
--colour-primary-950: #0A353A;
--colour-primary-light: #54C6CC;
<body class="h-full bg-gray-50"> --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> <script>
var api_url = '{{ api_url }}'; var api_url = "{{ api_url }}";
// Actions // Actions
$(document).ready(function () { $(document).ready(function () {
check_flow_expired(); check_flow_expired();
}); });
</script> </script>
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <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
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
id="contentFlowExpired"
class="alert alert-warning"
<a href="{{ dashboard_url }}"><img class="mx-auto h-10 w-auto mb-4" src='static/logo.svg' /></a> style="display: none"
<!-- <h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Your Stackspin >
account</h2> --> Your request has expired. Please resubmit your request.
</div>
<div id='messages'></div> <div class="login-header flex flex-col place-items-center">
<a href="{{ dashboard_url }}" class="block">
{% block content %}{% endblock %} <img class="stackspin-logo mx-auto h-10 w-auto" src="static/logo.svg" />
</div> </a>
</div> {% if demo %}
\ No newline at end of file <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 @@ ...@@ -6,9 +6,9 @@
// Wipe the local storage // Wipe the local storage
localStorage.removeItem("persist:root"); localStorage.removeItem("persist:root");
// Redirect // Redirect
window.location = '{{ url }}'; window.location = '{{ url | safe }}';
</script> </script>
Redirecting ... Clearing session data and redirecting...
{% endblock %} {% endblock %}
This diff is collapsed.
...@@ -23,17 +23,21 @@ ...@@ -23,17 +23,21 @@
<a class="nav-link" id="pills-password-tab" data-toggle="pill" href="#pills-password" role="tab" <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> 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" <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>
<div class="tab-content" id="pills-tabContent"> <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 class="tab-pane fade show active" id="pills-profile" role="tabpanel" aria-labelledby="pills-profile-tab">...
</div> </div>
<div class="tab-pane fade" id="pills-password" role="tabpanel" aria-labelledby="pills-password-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-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>
<div id="contentProfile"></div> <div id="contentProfile"></div>
<div id="contentPassword"></div> <div id="contentPassword"></div>
<div id="contentTotp"></div> <div id="contentTotp"></div>
<div id="contentWebauthn"></div>
{% endblock %} {% endblock %}
\ No newline at end of file