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 1148 additions and 604 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;
}
......@@ -98,6 +98,12 @@ function flow_login() {
// Render login form (group: password)
var form_html = render_form(data, group, "login");
$("#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 +156,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 +184,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 +201,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 +225,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 +255,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();
......@@ -343,28 +363,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;
}
......@@ -381,6 +391,14 @@ function render_messages(data) {
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 +446,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 +488,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 +514,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,7 +534,24 @@ 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>`
);
}
......@@ -536,6 +576,9 @@ function getFormElement(node, context) {
if (context == "recovery") {
label = "Send recovery link";
}
if (name == "webauthn_remove") {
label = node.meta.label.text;
}
return (
`<div class="form-group flex justify-end py-2">
<input type="hidden" name="` +
......@@ -550,7 +593,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 +606,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 +616,8 @@ function getFormInput(
placeHolder,
help,
messages,
readonly
readonly,
autocomplete
) {
if (typeof help == "undefined" || help == null) {
help = "";
......@@ -579,6 +625,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 +665,9 @@ function getFormInput(
if (readonly) {
element += "readonly ";
}
if (! autocomplete) {
element += "autocomplete=\"off\" ";
}
element += ">";
if (help) {
......@@ -643,3 +695,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;
......
/** @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 %}
{% extends 'base.html' %}
{% block content %}
{% extends 'base.html' %} {% block content %}
<script>
var api_url = '{{ api_url }}';
var api_url = '{{ api_url }}';
// Actions
$(document).ready(function () {
flow_login();
});
// Actions
$(document).ready(function () {
flow_login();
});
// Render messages
{% for message in messages %}
renderMessage('{{ message['id'] }}', '{{ message['message'] }}', '{{ message['type'] }}');
{% endfor %}
{% for message in messages %}
renderMessage('{{ message['id'] }}', '{{ message['message'] }}', '{{ message['type'] }}');
{% endfor %}
// On demo instance, submit the sign-up form via ajax
// so we can show the result on the same page.
{% if demo %}
$(document).ready(function(){
$('#signup-form').submit((event) => {
event.preventDefault();
window.console.log("signup submit");
submitSignup();
});
});
{% endif %}
</script>
<div id="contentMessages"></div>
<div id="contentLogin_password"></div>
<div id="contentLogin_totp"></div>
<div id="contentHelp" class="flex flex-col font-medium text-primary-600 mt-0 pt-2 border-t-gray-50 border-t-2">
<a href='recovery' class="block w-full text-right hover:text-primary-700 mb-2">Set new
password</a><a href='https://stackspin.net'
class="block text-right w-full text-sm text-gray-400 hover:text-primary-500">What
is
Stackspin?</a>
</div>
{% if demo %}
<br>
<script>
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);
}
$(() => {
// Submit the sign-up form via ajax so we can show the result on the same
// page.
$('#signup-form').submit((event) => {
event.preventDefault();
window.console.log("signup submit");
submitSignup();
});
})
</script>
<h4>Sign up for this demo instance</h4>
Enter your email address here to create an account on this Stackspin
instance.
<div class="alert alert-warning" style="margin-top: 1em;">
Warning: this is a demo instance! That means that:
<ul>
<li>Anyone can create an account on this same instance, like yourself,
and will share the same set of users and data. So any data you create
or upload, including the email address you enter here, becomes
essentially public information.</li>
<li>Every night (Europe/Amsterdam time), this instance gets automatically
reset to an empty state, so any data you create or upload will be
destroyed.</li>
</ul>
<div
class="rounded-md bg-gray-50 border border-gray-300 outline outline-1 outline-primary-300 p-4 flex flex-col gap-4 text-xs text-gray-500"
>
<p>Please keep in mind this is a demo instance:</p>
<ul class="divide-y divide-gray-200">
<li class="py-2">Anyone can create an account, just like yourself.</li>
<li class="py-2">
Any data you upload, including your email address, can be accessible by
other users.
</li>
<li class="py-2">
The demo is automatically reset every night (Europe/Amsterdam time),
removing all users and their data.
</li>
</ul>
</div>
<h1 class="text-lg leading-6 font-bold text-gray-900">
Create your Stackspin demo account:
</h1>
<form id="signup-form">
<div class="form-group">
<label for="signup-email">Email address</label>
<input type="email" class="form-control" id="signup-email" name="signup-email"
placeholder="Your email address to sign up with.">
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Sign up">
<div id="signup-result" class="alert" style="visibility: hidden; margin-top: 1em;"></div>
</div>
<label for="signup-email">E-mail address</label>
<div class="form-group flex justify-between gap-4">
<input
type="email"
class="form-control"
id="signup-email"
name="signup-email"
placeholder="Please provide your e-mail address"
/>
<button
type="submit"
class="inline-flex pointer 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"
>
Create
</button>
</div>
<div
id="signup-result"
class="form-group rounded-md bg-gray-50 border border-gray-300 outline outline-1 outline-primary-300 p-4 flex flex-col gap-4 text-xs text-gray-500"
style="visibility: hidden"
></div>
</form>
<h1
class="text-lg mt-0 leading-6 font-bold text-gray-900 pt-4 border-t-[1px] border-primary-400"
>
If you already have an account, log in:
</h1>
{% endif %}
{% endblock %}
\ No newline at end of file
<div id="contentMessages"></div>
<div id="contentLogin_password"></div>
<div id="contentLogin_totp"></div>
<div id="contentLogin_webauthn"></div>
<footer
class="font-medium text-primary-600 mt-0 pt-2 border-t-gray-50 border-t-2 flex justify-end"
>
<div id="contentHelp" class="flex flex-col text-right">
<a id="recoveryLink" href="recovery" class="hover:text-primary-700 mb-2">Set new password</a
><a
href="https://stackspin.net"
class="text-sm text-gray-400 hover:text-primary-500"
>What is Stackspin?</a
>
</div>
</footer>
{% endblock %}
This diff is collapsed.
FROM nginx:latest
FROM nginx:1.27
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY . /usr/share/nginx/html
COPY html /usr/share/nginx/html