diff --git a/README.md b/README.md index d5b0e51ec2a0eb74f69f1718a2e81fe395278e7b..f7b4959eb46d8e00c4ec1f33c2c0f731a1ac0687 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,10 @@ These need to be available locally, because Kratos wants to run on the same domain as the front-end that serves the login interface. ### Setup +Before you start, make sure your machine has the required software installed, as per official documentation: https://docs.stackspin.net/en/v2/installation/install_cli.html#preparing-the-provisioning-machine. Please read through all subsections to set up your environment before -attempting to run the dashboard locally. +attempting to run the dashboard locally. #### 1. Stackspin cluster diff --git a/backend/areas/users/users.py b/backend/areas/users/users.py index baaa9003538da7858ff3d47cd15d7e9626898722..25d906e593b405cb9a6a19f21a44f123194c181a 100644 --- a/backend/areas/users/users.py +++ b/backend/areas/users/users.py @@ -1,7 +1,7 @@ from flask import jsonify, request -from flask_jwt_extended import get_jwt, jwt_required from flask_cors import cross_origin from flask_expects_json import expects_json +from flask_jwt_extended import get_jwt, jwt_required from areas import api_v1 from helpers import KratosApi diff --git a/backend/config.py b/backend/config.py index 1972c59497b1229589837c66ca6973bb1e7fd408..6e5d37ac1221925e8403612674225bd0ccf5314d 100644 --- a/backend/config.py +++ b/backend/config.py @@ -21,3 +21,5 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False # running in a Kubernetes pod. Set it to "false" to load the config from the # `KUBECONFIG` environment variable. LOAD_INCLUSTER_CONFIG = os.environ.get("LOAD_INCLUSTER_CONFIG").lower() == "true" + +DEMO_INSTANCE = os.environ.get("DASHBOARD_DEMO_INSTANCE", "False").lower() in ('true', '1') diff --git a/backend/web/login/login.py b/backend/web/login/login.py index 12d4ff1aecaa1aa69fc960ff9033aa30b09006bf..25f09a006ace2a225fdf17d3f674a810a6a64fa4 100644 --- a/backend/web/login/login.py +++ b/backend/web/login/login.py @@ -16,7 +16,7 @@ from ory_hydra_client.models import AcceptConsentRequest, AcceptLoginRequest, Co import ory_hydra_client.exceptions as hydra_exceptions import ory_kratos_client from ory_kratos_client.api import frontend_api, identity_api -from flask import abort, redirect, render_template, request, current_app +from flask import abort, current_app, jsonify, redirect, render_template, request from database import db from helpers import KratosUser @@ -24,6 +24,8 @@ from config import * from web import web from areas.apps import AppRole, App, OAuthClientApp from areas.roles import RoleService +from areas.roles.models import Role +from areas.users.user_service import UserService # This is a circular import and should be solved differently @@ -159,7 +161,7 @@ def login(): # or `not identity` # User is not logged in yet. # In either case, we present the login screen now. - return render_template("login.html", api_url=KRATOS_PUBLIC_URL, dashboard_url=DASHBOARD_URL, refresh=refresh) + return render_template("login.html", api_url=KRATOS_PUBLIC_URL, dashboard_url=DASHBOARD_URL, refresh=refresh, demo=DEMO_INSTANCE) @web.route("/auth", methods=["GET", "POST"]) @@ -543,3 +545,14 @@ def logout(): ex) return redirect(kratos_api_response.logout_url) + +if DEMO_INSTANCE: + @web.route("/demo-user", methods=["POST"]) + def demo_user(): + data = request.get_json() + defaults = { + "name": "", + "app_roles": [{"name": "dashboard", "role_id": Role.ADMIN_ROLE_ID}], + } + UserService.post_user({**defaults, **data}) + return jsonify("User created successfully. You should receive an email to confirm your address and set a password.") diff --git a/backend/web/static/base.js b/backend/web/static/base.js index 566f8db7cfac424c70e9290ab45ff35dc383b57a..e7cab4a0d962dec9018d14fe703cb784a1a612da 100644 --- a/backend/web/static/base.js +++ b/backend/web/static/base.js @@ -59,6 +59,9 @@ function flow_login() { var messages_html = render_messages(data); $('#contentMessages').html(messages_html); + + $('#totp_code').focus(); + $('#identifier').focus(); }, complete: function (obj) { // If we get a 410, the flow is expired, need to refresh the flow diff --git a/backend/web/templates/login.html b/backend/web/templates/login.html index 515472f1103544e17d36b44d9d67665c09db8c0d..11d9f8a5315dfb97662604ad906c3022e2729971 100644 --- a/backend/web/templates/login.html +++ b/backend/web/templates/login.html @@ -24,5 +24,63 @@ <div id="contentHelp"> <a href='recovery'>Set new password</a> | <a href='https://stackspin.net'>About 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'); + 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); + } + </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> + <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"> + <button class="btn btn-primary" onclick="submitSignup()">Sign up</button> + <div id="signup-result" class="alert" style="visibility: hidden; margin-top: 1em;"></div> + </div> +{% endif %} {% endblock %} diff --git a/docker-compose.yml b/docker-compose.yml index d355fdb7103926167085d674b0df9a0a1623c0f5..655be3e58c31e6d70fefdab2b1f40398e6fee636 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: - kube_port_mysql entrypoint: ["bash", "-c", "flask run --host $$(hostname -i)"] kube_port_kratos_admin: - image: bitnami/kubectl:1.26.1 + image: bitnami/kubectl:1.26.2 user: "${KUBECTL_UID}:${KUBECTL_GID}" expose: - 8000 @@ -64,7 +64,7 @@ services: - "$KUBECONFIG:/.kube/config" entrypoint: ["bash", "-c", "kubectl -n stackspin port-forward --address $$(hostname -i) service/kratos-admin 8000:80"] kube_port_hydra_admin: - image: bitnami/kubectl:1.26.1 + image: bitnami/kubectl:1.26.2 user: "${KUBECTL_UID}:${KUBECTL_GID}" expose: - 4445 @@ -72,7 +72,7 @@ services: - "$KUBECONFIG:/.kube/config" entrypoint: ["bash", "-c", "kubectl -n stackspin port-forward --address $$(hostname -i) service/hydra-admin 4445:4445"] kube_port_kratos_public: - image: bitnami/kubectl:1.26.1 + image: bitnami/kubectl:1.26.2 user: "${KUBECTL_UID}:${KUBECTL_GID}" ports: - "8080:8080" @@ -82,7 +82,7 @@ services: - "$KUBECONFIG:/.kube/config" entrypoint: ["bash", "-c", "kubectl -n stackspin port-forward --address 0.0.0.0 service/kratos-public 8080:80"] kube_port_mysql: - image: bitnami/kubectl:1.26.1 + image: bitnami/kubectl:1.26.2 user: "${KUBECTL_UID}:${KUBECTL_GID}" expose: - 3306 diff --git a/frontend/src/components/UserModal/UserModal.tsx b/frontend/src/components/UserModal/UserModal.tsx index 900ef7aee10a0d8b8b84a1b92f043eee0c906c26..b6ceed73710f45f3bda1e4218224c85fbec575fd 100644 --- a/frontend/src/components/UserModal/UserModal.tsx +++ b/frontend/src/components/UserModal/UserModal.tsx @@ -236,6 +236,11 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) </select> </div> </div> + {userId && ( + <div className="sm:col-span-6"> + <Input control={control} name="id" label="UUID" required={false} disabled /> + </div> + )} </> )} </div> diff --git a/frontend/src/services/auth/redux/selectors.ts b/frontend/src/services/auth/redux/selectors.ts index 0e7d26b18e69382629cd6e315e628b7efa1a82db..0feaae547d0ed851a9a8b1b5d3308a2ea6633f87 100644 --- a/frontend/src/services/auth/redux/selectors.ts +++ b/frontend/src/services/auth/redux/selectors.ts @@ -10,7 +10,6 @@ export const getAuthToken = (state: State) => state.auth.token; export const getCurrentUser = (state: State) => state.auth.userInfo; export const getIsAdmin = (state: State) => { - window.console.log(state.auth.userInfo); // check since old users wont have this if (state.auth.userInfo) { if (!state.auth.userInfo.app_roles) { diff --git a/renovate.json b/renovate.json index 70fbf6501a77c71f7f5faa476bbc18c566fc08fc..3644c8c9361cc137031cbd352145bb7f4dade9be 100644 --- a/renovate.json +++ b/renovate.json @@ -5,5 +5,12 @@ ], "npm": { "enabled": false - } + }, + "packageRules": [ + { + "matchDepNames": ["node"], + "matchFiles": ["frontend/Dockerfile", ".gitlab-ci.yml"], + "allowedVersions": "!/^\d*[13579](-.*)?$/" + } + ] }