diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7559e10 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,3 @@ +FROM quay.io/keycloak/keycloak:latest + +COPY ["./juice", "/opt/jboss/keycloak/themes/juice/"] \ No newline at end of file diff --git a/README.md b/README.md index 0435704..549eeb5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,45 @@ -# keycloak-themed +# Keycloak image with juice theme inside +A short project that inject juice theme into a docker image. +Juice theme should be located in `./juice` folder. + +!!!IMPORTANT!!!: IF YOU MAKE AN UPDATE, PLEASE UPDATE THE VERSION.INI FILE ACORDING TO STANDARD VERSIONING RULES (NUMBERS ONLY). + +## Docker commands + +### Build the image +Build an image with tag juice-keycloak. + +```bash +docker build -t juice-keycloak . +``` + +### Stop previously running container +```bash +docker stop juice-keycloak +``` + +### Start a container +```bash +docker run --rm -it -d \ + -p 8080:8080 \ + -e KEYCLOAK_USER=admin \ + -e KEYCLOAK_PASSWORD=admin \ + -e DB_VENDOR=postgres \ + -e DB_ADDR=172.17.0.1 \ + -e DB_DATABASE=keycloak \ + -e DB_USER=keycloak \ + -e DB_PASSWORD=keycloak \ + --name juice-keycloak \ + juice-keycloak +``` + +Linux: PGHOST=172.17.0.1 +Mac&Win: PGHOST=host.docker.internal + +## Testing +There is a file `url.test.js` that will generate a login URL. After sucessfull login, provide the redirect url from the browser as an argument for the script to acquire tokens. + +The script includes installable package listed at the top. + +Change configuration inside of the script according to your realm settings. \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..ebf78ea --- /dev/null +++ b/build.sh @@ -0,0 +1,63 @@ +#!/bin/bash +function cleanup { + if [ ! -z "${RPM_BUILD_ROOT}" ] && [ -d "${RPM_BUILD_ROOT}" ] && [ "${RPM_BUILD_ROOT}" != "./" ] + then + rm -rf "${RPM_BUILD_ROOT}" + fi +} +export SOURCE_DIR=`dirname $0` +echo "${SOURCE_DIR}" |grep -q "^\." +if [ $? -eq 0 ] +then + echo "sourcdir is ." + export SOURCE_DIR="`pwd`/" +fi +echo "${SOURCE_DIR}" |grep -q "^/" +if [ $? -ne 0 ] +then + export SOURCE_DIR="`pwd`/`dirname $0`" +fi +echo "SOURCE_DIR: ${SOURCE_DIR}" +export RELEASE_VERSION=`cat version.ini` +if [ "${docker_registry}" != "" ] || [ "${1}" == "docker-image" ] || [ "${2}" == "docker-image" ] +then + docker build -t ${docker_registry}${docker_registry_path_suffix}/juice-keycloak:${RELEASE_VERSION} -t ${docker_registry}${docker_registry_path_suffix}/juice-keycloak:latest . && + docker login --username ${docker_registry_username} --password ${docker_registry_password} ${docker_registry} && + docker push ${docker_registry}${docker_registry_path_suffix}/juice-keycloak:${RELEASE_VERSION} && + docker push ${docker_registry}${docker_registry_path_suffix}/juice-keycloak:latest + exit $? +fi + + +if [ "${BUILD_RPM}" != "" ] || [ "${1}" == "rpm" ] || [ "${2}" == "rpm" ] +then + + export _RPMBUILD_DIR="${RPMBUILD_DIR:-/tmp/rpmbuild-juice-keycloak}" + export RPM_BUILD_ROOT="${RPM_BUILD_ROOT:-/tmp/build-juice-keycloak}" + if [ ! -d "${_RPMBUILD_DIR}" ] + then + mkdir -p "${_RPMBUILD_DIR}" + mkdir -p "${_RPMBUILD_DIR}"/RPMS/noarch + mkdir -p "${_RPMBUILD_DIR}"/SOURCES + mkdir -p "${_RPMBUILD_DIR}"/SPECS + mkdir -p "${_RPMBUILD_DIR}"/SRPMS + fi + if [ ! -d "${RPM_BUILD_ROOT}" ] + then + mkdir -p "${RPM_BUILD_ROOT}" + fi + rpmbuild --target noarch -bb "${SOURCE_DIR}/juice-keycloak.spec" --buildroot="${RPM_BUILD_ROOT}" --define "_topdir ${_RPMBUILD_DIR}" --define "_release_version ${RELEASE_VERSION}" +fi + +if [ "${RUN_HOOKS}" != "" ] +then + if [ "${BUILD_RPM}" != "" ] || [ "${1}" == "rpm" ] || [ "${2}" == "rpm" ] + then + run-parts "${SOURCE_DIR}/build-hooks/rpm" + fi + if [ "${docker_registry}" != "" ] || [ "${1}" == "docker-image" ] || [ "${2}" == "docker-image" ] + then + run-parts "${SOURCE_DIR}/build-hooks/docker-image" + fi +fi +cleanup \ No newline at end of file diff --git a/juice/account/resources/css/account.css b/juice/account/resources/css/account.css new file mode 100644 index 0000000..3878e43 --- /dev/null +++ b/juice/account/resources/css/account.css @@ -0,0 +1,277 @@ +html { + height: 100%; +} + +body { + background-color: #F9F9F9; + margin: 0; + padding: 0; + height: 100%; +} + +header .navbar { + margin-bottom: 0; + min-height: inherit; +} + +.header .container { + position: relative; +} + +.navbar-title { + background-image: url('../img/logo.png'); + height: 25px; + background-repeat: no-repeat; + width: 123px; + margin: 3px 10px 5px; + text-indent: -99999px; +} + +.navbar-pf .navbar-utility { + right: 20px; + top: -34px; + font-size: 12px; +} + +.navbar-pf .navbar-utility > li > a { + color: #fff !important; + padding-bottom: 12px; + padding-top: 11px; + border-left: medium none; +} + +.container { + height: 100%; +} + +.content-area { + background-color: #fff; + border-color: #CECECE; + border-style: solid; + border-width: 0 1px; + height: 100%; + padding: 0 30px; +} + +.margin-bottom { + margin-bottom: 10px; +} + +/* Sidebar */ + +.bs-sidebar { + background-color: #f9f9f9; + padding-top: 44px; + padding-right: 0; + padding-left: 0; + z-index: 20; +} +.bs-sidebar ul { + list-style: none; + padding-left: 12px; +} + +.bs-sidebar ul li { + margin-bottom: 0.5em; + margin-left: -1em; +} +.bs-sidebar ul li a { + font-size: 14px; + padding-left: 25px; + color: #4d5258; + line-height: 28px; + display: block; + border-width: 1px 0 1px 1px; + border-style: solid; + border-color: #f9f9f9; +} +.bs-sidebar ul li a:hover, +.bs-sidebar ul li a:focus { + text-decoration: none; + color: #777777; + border-right: 2px solid #aaa; +} +.bs-sidebar ul li.active a { + background-color: #c7e5f0; + border-color: #56bae0; + font-weight: bold; + background-image: url(../img/icon-sidebar-active.png); + background-repeat: no-repeat; + background-position: right center; +} + +.bs-sidebar ul li.active a:hover { + border-right: none; +} + + +.content-area h2 { + font-family: "Open Sans", sans-serif; + font-weight: 100; + font-size: 24px; + margin-bottom: 25px; + margin-top: 25px; +} + +.subtitle { + text-align: right; + margin-top: 30px; + color: #909090; +} + +.required { + color: #CB2915; +} + + +.alert { + margin-top: 30px; + margin-bottom: 0; +} + +.feedback-aligner .alert { + background-position: 1.27273em center; + background-repeat: no-repeat; + border-radius: 2px; + border-width: 1px; + color: #4D5258; + display: inline-block; + font-size: 1.1em; + line-height: 1.4em; + margin: 0; + padding: 0.909091em 3.63636em; + position: relative; + text-align: left; +} +.alert.alert-success { + background-color: #E4F1E1; + border-color: #4B9E39; +} +.alert.alert-error { + background-color: #F8E7E7; + border-color: #B91415; +} +.alert.alert-warning { + background-color: #FEF1E9; + border-color: #F17528; +} +.alert.alert-info { + background-color: #E4F3FA; + border-color: #5994B2; +} + +.form-horizontal { + border-top: 1px solid #E9E8E8; + padding-top: 23px; +} + +.form-horizontal .control-label { + color: #909090; + line-height: 1.4em; + padding-top: 5px; + position: relative; + text-align: right; + width: 100%; +} + +.form-group { + position: relative; +} + +.control-label + .required { + position: absolute; + right: -2px; + top: 0; +} + +#kc-form-buttons { + text-align: right; + margin-top: 10px; +} + +#kc-form-buttons .btn-primary { + float: right; + margin-left: 8px; +} + +/* Authenticator page */ + +ol { + padding-left: 40px; +} + +ol li { + font-size: 13px; + margin-bottom: 10px; + position: relative; +} + +ol li img { + margin-top: 15px; + margin-bottom: 5px; + border: 1px solid #eee; +} + +hr + .form-horizontal { + border: none; + padding-top: 0; +} + +.kc-dropdown{ + position: relative; +} +.kc-dropdown > a{ + display:block; + padding: 11px 10px 12px; + line-height: 12px; + font-size: 12px; + color: #fff !important; + text-decoration: none; +} +.kc-dropdown > a::after{ + content: "\2c5"; + margin-left: 4px; +} +.kc-dropdown:hover > a{ + background-color: rgba(0,0,0,0.2); +} +.kc-dropdown ul li a{ + padding: 1px 11px; + font-size: 12px; + color: #000 !important; + border: 1px solid #fff; + text-decoration: none; + display:block; + line-height: 20px; +} +.kc-dropdown ul li a:hover{ + color: #4d5258; + background-color: #d4edfa; + border-color: #b3d3e7; +} +.kc-dropdown ul{ + position: absolute; + z-index: 2000; + list-style:none; + display:none; + padding: 5px 0px; + margin: 0px; + background-color: #fff !important; + border: 1px solid #b6b6b6; + border-radius: 1px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; + min-width: 100px; +} +.kc-dropdown:hover ul{ + display:block; +} + + +#kc-totp-secret-key { + border: 1px solid #eee; + font-size: 16px; + padding: 10px; + margin: 50px 0; +} \ No newline at end of file diff --git a/juice/account/resources/img/icon-sidebar-active.png b/juice/account/resources/img/icon-sidebar-active.png new file mode 100644 index 0000000..e7b9b08 Binary files /dev/null and b/juice/account/resources/img/icon-sidebar-active.png differ diff --git a/juice/account/resources/img/keycloak-logo.png b/juice/account/resources/img/keycloak-logo.png new file mode 100644 index 0000000..9555748 Binary files /dev/null and b/juice/account/resources/img/keycloak-logo.png differ diff --git a/juice/account/resources/img/logo.png b/juice/account/resources/img/logo.png new file mode 100644 index 0000000..a698c54 Binary files /dev/null and b/juice/account/resources/img/logo.png differ diff --git a/juice/account/theme.properties b/juice/account/theme.properties new file mode 100644 index 0000000..e7f1147 --- /dev/null +++ b/juice/account/theme.properties @@ -0,0 +1,14 @@ +parent=base +import=common/keycloak + +styles=css/account.css +stylesCommon=node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css + +##### css classes for form buttons +# main class used for all buttons +kcButtonClass=btn +# classes defining priority of the button - primary or default (there is typically only one priority button for the form) +kcButtonPrimaryClass=btn-primary +kcButtonDefaultClass=btn-default +# classes defining size of the button +kcButtonLargeClass=btn-lg diff --git a/juice/admin/resources/css/styles.css b/juice/admin/resources/css/styles.css new file mode 100644 index 0000000..681b3fc --- /dev/null +++ b/juice/admin/resources/css/styles.css @@ -0,0 +1,480 @@ +html,body { + height: 100%; +} + +form { + margin-top: 20px; +} + +table { + margin-top: 20px; +} + +.required { + color: #f00; +} + +.tooltip-inner { + min-width: 200px; +} + +.margin-top { + margin-top: 20px; +} + +.no-margin-top { + margin-top: 0px !important; +} + +table { + max-width: 100%; +} + +td.clip { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 0; +} + +th.w-10 { + width: 10%; +} + +th.w-15 { + width: 15%; +} + +th.w-20 { + width: 20%; +} + + +th.w-25 { + width: 25%; +} + +th.w-30 { + width: 30%; +} + + +th.w-35 { + width: 35%; +} + +th.w-40 { + width: 40%; +} + +/*********** Loading ***********/ + +.loading { + background-color: #f5f5f5; + border: 1px solid #eee; + position: absolute; + bottom: 0px; + left: 0px; + padding: 2px 200px 2px 5px; +} + +/*********** Feedback ***********/ + +.feedback-aligner { + position: fixed; + top: 15px; + text-align: center; + width: 100%; + height: 0; + z-index: 100; +} +.feedback-aligner .alert { + border-radius: 2px; + border-width: 1px; + display: inline-block; + position: relative; +} + +/*********** On-Off Switch ***********/ + +.onoffswitch { + -moz-user-select: none; + height: 26px; + position: relative; + width: 62px; +} +.onoffswitch .onoffswitch-checkbox { + display: none; +} +.onoffswitch .onoffswitch-label { + border: 1px solid #bbb; + border-radius: 2px; + cursor: pointer; + display: block; + overflow: hidden; + width: 62px; +} +.onoffswitch .onoffswitch-inner { + display: block; + margin-left: -100%; + transition: margin 0.3s ease-in 0s; + width: 200%; +} +.onoffswitch .onoffswitch-inner > span { + -moz-box-sizing: border-box; + color: white; + float: left; + font-size: 11px; + font-family: "Open Sans", sans-serif; + font-weight: bold; + height: 24px; + line-height: 24px; + padding: 0; + width: 50%; +} +.onoffswitch .onoffswitch-switch { + background-image: linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -o-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -moz-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -webkit-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -ms-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fafafa), color-stop(1, 0, #ededed)); + border: 1px solid #aaa; + border-radius: 2px; + bottom: 0; + margin: 0; + position: absolute; + right: 39px; + top: 0; + transition: all 0.3s ease-in 0s; + -webkit-transition: all 0.3s ease-in 0s; + width: 23px; +} +.onoffswitch .onoffswitch-inner .onoffswitch-active { + background-image: linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -o-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -moz-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -webkit-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -ms-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #00a9ec), color-stop(1, 0, #009bd3)); + color: #FFFFFF; + padding-left: 10px; +} +.onoffswitch-checkbox:disabled + .onoffswitch-label .onoffswitch-inner .onoffswitch-active, +.onoffswitch-checkbox:disabled + .onoffswitch-label .onoffswitch-inner .onoffswitch-inactive { + background-image: none; + background-color: #e5e5e5; + color: #9d9fa1; +} +.onoffswitch .onoffswitch-inner .onoffswitch-inactive { + background: linear-gradient(#fefefe, #e8e8e8) repeat scroll 0 0 transparent; + color: #4d5258; + padding-right: 10px; + text-align: right; +} +.onoffswitch .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { + margin-left: 0; +} +.onoffswitch .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { + right: 0; +} + + +/*********** Select 2 ***********/ + +.select2-container { + width: 100%; +} + +.select2-container-multi .select2-choices .select2-search-field { + height: 26px; +} + +/*********** html select ********/ +.overflow-select { + overflow: auto; +} + + +/*********** New Menu ***********/ + + +.sidebar-pf-left{ + background: #292e34; +} + +.sidebar-pf .nav-pills > li a i, .sidebar-pf .nav-pills > li a span{ + color: #72767b; + display: inline-block; + margin-right: 10px; +} +.sidebar-pf .nav-pills > li > a{ + color: #dbdada; + padding: 0px 20px 0 30px!important; + line-height: 30px; + border-left-width: 12px; + border-left-style: solid; + border-left-color: #292e34; + margin-left: -6px; +} + +.sidebar-pf .nav-pills > li > a:hover{ + background: #393f44; + border-color:#292e34; + border-left-color: #393f44; + color: #fff; +} + +.sidebar-pf .nav-pills > li > a:after{ + display: none!important; +} + + +.sidebar-pf .nav-pills > li.active > a { + color: #fff; + background: #393f44!important; + border-bottom: 1px solid #000!important; + border-top: 1px solid #000!important; + border-left-color: #39a5dc!important; +} + +.sidebar-pf .nav-pills > li.active a i, .sidebar-pf .nav-pills > li.active a span{ + color: #39a5dc; +} + +/*********** Realm selector ***********/ + +.realm-selector{ + color: #fff; + margin: 0 -20px; + position: relative; +} + +.realm-dropmenu{ + display: none; + cursor: pointer; + position: absolute; + top: 60px; + left: 0; + right: 0; + z-index: 999; + background: #fff; +} + +.realm-selector:hover .realm-dropmenu{ + display: block; +} + +.realm-add{ + padding: 10px; +} + +.realm-selector h2{ + font-size: 16px; + line-height: 60px; + padding: 0 20px; + margin: 0; + border-bottom: 1px solid #d5d5d6; +} + +.realm-selector h2 i{ + display: inline-block; + float: right; + line-height: 60px; +} + + +.realm-selector ul{ + padding-left: 0; + margin: 0; + list-style: none; + max-height: 200px; + overflow-y:auto; +} + + +.realm-selector ul li a{ + line-height: 60px; + padding: 0 20px; + border-bottom: 1px solid #d5d5d6; + line-height: 39px; + display: block; + font-size: 14px; +} + + +/*********** Overwrites header defaults ***********/ + +.navbar-pf{ + border-top: none!important; +} + +.navbar-pf .navbar-brand { + padding: 0; + height: 56px; + line-height: 56px; + background-position: center center; + background-image: url('../img/keyclok-logo.png'); + background-size: 148px 30px; + background-repeat: no-repeat; + width: 148px; +} + +.navbar-pf .navbar-utility .dropdown-toggle { + padding: 23px !important; +} + +.clickable { + cursor: pointer; +} + +h1 i { + color: #999999; + font-size: 18px; + margin-left: 10px; +} + +/* Action cell */ +.kc-action-cell { + background-color: #eeeeee; + background-image: linear-gradient(to bottom, #fafafa 0%, #ededed 100%); + background-repeat: repeat-x; + + text-align: center; + vertical-align: middle; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + cursor:pointer; +} + +.kc-action-cell:hover { + background-color: #eeeeee; + background-image: none; +} + +.kc-sorter span { + margin-left: 10px; +} + + +/* Time selector */ + +.time-selector input { + display: inline-block; + width: 120px; + padding-right: 0; + margin-right: 0; +} + +.time-selector select { + display: inline-block; + width: 80px; + margin-left: 0; + padding-left: 0; +} + +.ace_editor { + height: 600px; + width: 100%; +} + +.kc-button-input-file input { + float: left; + width: 73%; +} + +.kc-button-input-file label { + float: left; + margin-left: 2%; + width: 25%; +} + +table.kc-authz-table-expanded { + margin-top: 0px !important; +} + +.no-gutter > [class*='col-'] { + padding-right:0!important; + padding-left:0!important; +} + +.password-conceal { + font-family: 'text-security-disc'; + font-size: 14px; +} + +/* Deactivation styles for user-group membership tree models */ + +div[tree-model] li .deactivate { + color: #4a5053; + opacity: 0.4; +} + +div[tree-model] li .deactivate_selected { + background-color: #dcdcdc; + font-weight: bold; + padding: 1px 5px; +} + +/* search highlighting */ + +div[tree-model] li .highlight { + background-color: #aaddff; +} + +/* Manage credentials */ +table.credentials-table { + margin-top: 0; + margin-bottom: 20px; +} + +table.credentials-table td { + vertical-align: middle !important; +} + +table.credentials-table input[type='text'] { + width: 100%; +} + +td.credential-arrows-cell { + width: 75px; +} + +td.credential-label-cell { + padding: 5px !important; +} + +td.credential-action-cell { + padding: 0px !important; +} + +td.credential-action-cell div.kc-action-cell { + width: 100%; + height: 36px; + line-height: 34px; +} + +td.credential-action-cell.expanded div.kc-action-cell { + border-bottom: 1px solid #d1d1d1; +} + +table.credential-data-table td { + word-break: break-all; +} + +table.credential-data-table tr:first-child td { + border-top: 0; +} + +table.credential-data-table td:first-child { + width: 150px; +} + +table.credential-data-table td.key { + text-align: right; + font-weight: bold; +} + diff --git a/juice/admin/resources/img/keyclok-logo.png b/juice/admin/resources/img/keyclok-logo.png new file mode 100644 index 0000000..ca53f0a Binary files /dev/null and b/juice/admin/resources/img/keyclok-logo.png differ diff --git a/juice/admin/resources/img/keyclok-logo.svg b/juice/admin/resources/img/keyclok-logo.svg new file mode 100644 index 0000000..05fa87b --- /dev/null +++ b/juice/admin/resources/img/keyclok-logo.svg @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/juice/admin/resources/img/select-arrow.png b/juice/admin/resources/img/select-arrow.png new file mode 100644 index 0000000..a865a6f Binary files /dev/null and b/juice/admin/resources/img/select-arrow.png differ diff --git a/juice/admin/theme.properties b/juice/admin/theme.properties new file mode 100644 index 0000000..d196266 --- /dev/null +++ b/juice/admin/theme.properties @@ -0,0 +1,5 @@ +parent=base +import=common/keycloak + +styles=css/styles.css css/mytheme.css +stylesCommon=node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css node_modules/select2/select2.css lib/angular/treeview/css/angular.treeview.css node_modules/text-security/text-security.css css/mytheme.css diff --git a/juice/email/theme.properties b/juice/email/theme.properties new file mode 100644 index 0000000..f1dbb72 --- /dev/null +++ b/juice/email/theme.properties @@ -0,0 +1 @@ +parent=base \ No newline at end of file diff --git a/juice/login/error.ftl b/juice/login/error.ftl new file mode 100644 index 0000000..7115b16 --- /dev/null +++ b/juice/login/error.ftl @@ -0,0 +1,13 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "header"> +
${msg("error")}
+
${msg("errorTitle")}
+
+ <#elseif section = "form"> +
+

${message.summary?no_esc}

+
+ + + diff --git a/juice/login/login-page-expired.ftl b/juice/login/login-page-expired.ftl new file mode 100644 index 0000000..1721b76 --- /dev/null +++ b/juice/login/login-page-expired.ftl @@ -0,0 +1,13 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> +
${msg("error")}
+
${msg("pageExpiredTitle")}
+
+ <#elseif section = "form"> +

+ ${msg("pageExpiredMsg1")} +

+ ${msg("pageExpiredButton")} + + diff --git a/juice/login/login-reset-password.ftl b/juice/login/login-reset-password.ftl new file mode 100644 index 0000000..6705aa3 --- /dev/null +++ b/juice/login/login-reset-password.ftl @@ -0,0 +1,61 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('username'); section> + <#if section = "header"> +
${msg("passwordRecovery")}
+
+ ${msg("emailForgotTitle")} +
+ ${msg("emailForgotTitle1")} +
+ <#elseif section = "form"> +
+
+ + + +
+ <#if auth?has_content && auth.showUsername()> + + + + <#else> + + + + + <#if messagesPerField.existsError('username')> +
+ ${kcSanitize(messagesPerField.get('username'))?no_esc} +
+ +
+ +
+ +
+ +
+
+ <#if client?? && client.baseUrl?has_content> + ${kcSanitize(msg("backToLogin"))?no_esc} + +
+
+
+ ${msg("haveAccount")} ${msg("signInHere")} +
+
+
+
+ +
+ <#elseif section = "info" > + + + + \ No newline at end of file diff --git a/juice/login/login-verify-email.ftl b/juice/login/login-verify-email.ftl new file mode 100644 index 0000000..48536d3 --- /dev/null +++ b/juice/login/login-verify-email.ftl @@ -0,0 +1,14 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayInfo=true; section> + <#if section = "header"> +
${msg("emailVerifyTitle")}
+
${msg("emailVerifyTitle")}
+
+ <#elseif section = "form"> +

${msg("emailVerifyInstruction1")}

+ <#elseif section = "info"> +
${msg("emailVerifyInstruction2")}
+ ${msg("emailVerifyInstruction3")} + + + \ No newline at end of file diff --git a/juice/login/login.ftl b/juice/login/login.ftl new file mode 100644 index 0000000..a789581 --- /dev/null +++ b/juice/login/login.ftl @@ -0,0 +1,122 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section> + <#if section = "header"> +
+
${msg("welcome")}
+ <#if realm.internationalizationEnabled && locale.supported?size gt 1> +
+
+
+ ${locale.current} +
    + <#list locale.supported as l> + <#if locale.current==l.label> +
  • ${l.label}
  • + <#else> +
  • ${l.label}
  • + + +
+
+
+
+ +
+
${msg("loginAccountTitle")}
+ <#elseif section = "form"> +
+
+ <#if realm.password> +
+
+ + <#if usernameEditDisabled??> + + <#else> + + <#if messagesPerField.existsError('username','password')> +
+ ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} +
+ + + + +
+ +
+ + + +
+ +
+ value="${auth.selectedCredential}"/> + +
+ + + <#if realm.password && social.providers??> + + + + + +
+
+
+ <#if realm.resetPasswordAllowed> + ${msg("doForgotPassword")} + +
+ <#if realm.password && realm.registrationAllowed && !registrationDisabled??> +
+
+ ${msg("noAccount")} ${msg("signUpHere")} +
+
+ +
+ + + +
+
+ + + + diff --git a/juice/login/messages/messages_de.properties b/juice/login/messages/messages_de.properties new file mode 100644 index 0000000..496ce3b --- /dev/null +++ b/juice/login/messages/messages_de.properties @@ -0,0 +1,35 @@ +welcome=Willkommen! +error=Fehler +signinTerms=Mit der Registrierung erkl\u00E4ren Sie sich mit den Nutzungsbedingungen und der Datenschutzerkl\u00E4rung von Juice einverstanden. +signupTerms=Mit der Registrierung erkl\u00E4ren Sie sich mit den Nutzungsbedingungen und der Datenschutzerkl\u00E4rung von Juice einverstanden. +signInHere=Hier anmelden +signUpHere=Mit Email registrieren +passwordRecovery=Passwort wiederherstellen + +loginAccountTitle=Anmelden +doRegister=Registrieren +doForgotPassword=Passwort vergessen? +noAccount=Haben Sie ein Konto? + +emailForgotTitle=Ihr Passwort +emailForgotTitle1=zur\u00FCcksetzen +emailInstruction=Bitte geben Sie Ihre E-Mail Adresse ein +emailInstruction1=Wir senden Ihnen dann eine E-Mail, um Ihr Passwort zur\u00FCckzusetzen. + +doSubmit=E-Mail senden +backToLogin=Zur\u00FCck +haveAccount=Haben Sie ein Konto? + +registerTitle=Welcome to the +registerTitle1=JUICE WORLD + +emailVerifyTitle=Verifizieren Sie Ihre E-Mail +emailVerifyInstruction1=Sie m\u00FCssen Ihre E-Mail-Adresse verifizieren, um Ihr Konto zu aktivieren. Eine E-Mail mit Anweisungen zur Verifizierung Ihrer E-Mail-Adresse wurde an Sie gesendet +emailVerifyInstruction3=Senden Sie die E-Mail erneut +identity-provider-login-label=Oder registrieren/anmelden mit + +pageExpiredTitle=Ihre Sitzung ist abgelaufen +pageExpiredMsg1=Keine Sorge, melden Sie sich einfach erneut an + +pageExpiredButton=Gehen Sie zu Anmelden + diff --git a/juice/login/messages/messages_en.properties b/juice/login/messages/messages_en.properties new file mode 100644 index 0000000..cade1f7 --- /dev/null +++ b/juice/login/messages/messages_en.properties @@ -0,0 +1,33 @@ +welcome=Welcome! +error=Error +signinTerms=By signing up, you agree to Juice''s ToS and Privacy +signupTerms=By signing up, you agree to Juice''s Terms of Use +signInHere=Sign in here +signUpHere=Sign up with the email +passwordRecovery=Password recovery + +loginAccountTitle=Sign in +doRegister=Sign up +doForgotPassword=Forgot password? +noAccount=Not a member? + +emailForgotTitle=Reset +emailForgotTitle1=your password +emailInstruction=Please enter your email address. +emailInstruction1=We will send you an email to reset your password. + +doSubmit=Send email +backToLogin=Go back +haveAccount=Have an account? + +registerTitle=Welcome to the +registerTitle1=JUICE WORLD + +emailVerifyTitle=Verify your email +emailVerifyInstruction1=You need to verify your email address to activate your account. An email with instructions to verify your email address has been sent to you +emailVerifyInstruction3=Re-send the email + +pageExpiredTitle=Your session has expired +pageExpiredMsg1=No worry, simply sign in again + +pageExpiredButton=Go to Sign In \ No newline at end of file diff --git a/juice/login/register.ftl b/juice/login/register.ftl new file mode 100644 index 0000000..03c8027 --- /dev/null +++ b/juice/login/register.ftl @@ -0,0 +1,139 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section> + <#if section = "header"> +
+ + <#if realm.internationalizationEnabled && locale.supported?size gt 1> +
+
+
+ ${locale.current} +
    + <#list locale.supported as l> + <#if locale.current==l.label> +
  • ${l.label}
  • + <#else> +
  • ${l.label}
  • + + +
+
+
+
+ +
+
${msg("registerTitle")}
${msg("registerTitle1")}
+ <#elseif section = "form"> +
+ +
+
+ + + + <#if messagesPerField.existsError('email')> +
+ ${kcSanitize(messagesPerField.get('email'))?no_esc} +
+ +
+ + <#if passwordRequired??> +
+ + + + <#if messagesPerField.existsError('password')> +
+ ${kcSanitize(messagesPerField.get('password'))?no_esc} +
+ +
+ +
+ + + + <#if messagesPerField.existsError('password-confirm')> +
+ ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc} +
+ +
+ +
+ + <#if !realm.registrationEmailAsUsername> +
+
+ +
+
+ + + <#if messagesPerField.existsError('username')> +
+ ${kcSanitize(messagesPerField.get('username'))?no_esc} +
+ +
+
+ + + <#if recaptchaRequired??> +
+
+
+
+
+ + +
+ +
+ +
+ + + +
+ + +
+
+ + <#if realm.password && realm.registrationAllowed && !registrationDisabled??> +
+
+ ${msg("haveAccount")} ${msg("signInHere")} +
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/juice/login/resources/css/styles.css b/juice/login/resources/css/styles.css new file mode 100644 index 0000000..b1027e4 --- /dev/null +++ b/juice/login/resources/css/styles.css @@ -0,0 +1,758 @@ +@font-face { + font-family: "Swis721 Th Bt"; + src: local("Swiss721ThinBT.ttf"), url("../fonts/Swiss721ThinBT.ttf") format("truetype"); + font-weight: 300; +} +@font-face { + font-family: "Swis721 Md BT"; + src: local("Swiss721MediumBT.ttf"), url("../fonts/Swiss721MediumBT.ttf") format("truetype"); + font-weight: 500; +} +@font-face { + font-family: "Roboto"; + src: local("Roboto-Medium.ttf"), url("../fonts/Roboto-Medium.ttf") format("truetype"); + font-weight: 300; +} + +.login-pf { + height: 100%; + background: none; +} + +.login-pf body { + background: url(../img/juice-bc.svg); + background-repeat: no-repeat; + background-color: #202020; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} +.login-pf body::before { + position: absolute; + width: 56px; + height: 56px; + content: ""; + top: 56px; + left: 63px; + background: url(../img/juice.svg); + background-repeat: no-repeat; +} + +.login-pf-page { + padding-top: 0px; +} + +.h1, .h2, .h3, h1, h2, h3 { + margin-top: 0px; + margin-bottom: 0px; +} + +#kc-header-wrapper { + padding: 0px 10px; +} + +.card-login { + margin: 0 auto; + box-shadow: -8px -8px 32px rgba(101, 101, 101, 0.12), -2px -2px 16px rgba(101, 101, 101, 0.24), 4px 4px 16px rgba(0, 0, 0, 0.3), 8px 8px 32px rgba(0, 0, 0, 0.1); + padding: 56px; + min-width: 440px; + max-width: 440px; + border-radius: 16px; + background-color: #212121; +} + +.login-pf-page-header { + background: url(../img/juice.svg); + background-position-x: 63px; + background-position-y: 56px; + background-repeat: no-repeat; +} +.header-login-welcome { + font-family: "Swis721 Th Bt"; + font-style: normal; + font-weight: 300; + font-size: 14px; + line-height: 20px; + color: #98989E; +} +.header-login-title { + padding-top: 4px; + font-family: "Swis721 Th Bt"; + font-style: normal; + font-weight: 300; + font-size: 38px; + line-height: 48px; + letter-spacing: -0.75px; + background: linear-gradient(180deg, #FF9900 0%, #FF7100 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.error-welcome { + background: #F52E2E; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} +.error-image { + width: 96px; + height: 96px; + background: url(../img/error.svg); + margin: 32px 0px 12px; +} +.verify-image { + width: 96px; + height: 96px; + background: url(../img/verify-email.svg); + margin: 32px 0px 12px; +} +.instruction { + margin: 0px; + font-family: "Swis721 Th BT"; + font-style: normal; + font-weight: 300; + font-size: 19px; + line-height: 28px; + display: flex; + align-items: center; + letter-spacing: -0.1px; + color: #BDBFBF; +} +#kc-error-message .instruction { + margin: 0px; + font-family: "Swis721 Th BT"; + font-style: normal; + font-weight: 300; + font-size: 19px; + line-height: 28px; + display: flex; + align-items: center; + letter-spacing: -0.1px; + color: #BDBFBF; +} +#kc-info { + margin: 0px; +} +.verify-email-question { + margin-top: 32px; + margin-bottom: 8px; + font-family: "Swis721 Md BT"; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 16px; + display: flex; + align-items: center; + color: #BDBFBF; +} +.login-pf-page .login-pf-signup a { + margin-left: 0px; +} +.login-pf-page .login-pf-settings { + flex-wrap: nowrap; +} +.verify-email-link { + margin-left: 0px; + font-family: "Swis721 Md BT"; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 16px; + display: flex; + align-items: center; + text-align: right; + color: #FF7100; +} +a.verify-email-link:hover { + color: #FF7100; +} + +#kc-page-title{ + text-align: left; +} + +#kc-form-buttons { + margin-top: 32px; + margin-bottom: 32px; +} + +.pf-c-button { + height: 56px; + background: #181818; + box-shadow: -4px -4px 8px rgba(101, 101, 101, 0.08), -1px -1px 3px rgba(101, 101, 101, 0.12), 1px 1px 4px rgba(0, 0, 0, 0.3), 4px 4px 8px rgba(0, 0, 0, 0.4); + border-radius: 4px; + font-family: "Swis721 Md BT"; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + text-align: center; + color: #BDBFBF; + text-transform: capitalize; +} +.pf-c-button:hover, +.pf-c-button:focus { + background: #292929; +} +.pf-c-button:disabled { + background: #292929; + opacity: 0.3; +} + +.pf-c-form-control{ + position: relative; + min-height: 56px; + border: 1px solid #333333; + box-sizing: border-box; + border-radius: 4px; + background: #212121; + font-family: "Swis721 Th BT"; + padding-left: 16px; + font-style: normal; + font-weight: 300; + font-size: 16px; + line-height: 24px; + color: #BDBFBF; +} + +.form-group { + position: relative; + margin-bottom: 24px; +} + +#input-error, +#input-error-username, +#input-error-email, +#input-error-password, +#input-error-password-confirm { + margin-top: 8px; + font-family: "Swis721 Th BT"; + font-style: normal; + font-weight: 300; + font-size: 14px; + line-height: 20px; + color: #BDBFBF; +} + + +.pf-c-form-control:focus, .pf-c-form-control:hover{ + outline: none; + border: 1px solid rgba(255, 113, 0, 1); + caret-color: rgba(255, 113, 0, 1); + font-family: Swis721 Th BT; +} +.pf-c-form-control[aria-invalid="true"] { + border: 1px solid rgba(245, 46, 46, 1); + background: #212121; +} +.login-form-input-label { + position: absolute; + display: none; + padding: 0px 4px; + top: -10px; + left: 12px; + color:rgba(255, 113, 0, 1); + z-index: 100; + background-color: #212121; +} +.pf-c-form-control:focus ~ .login-form-input-label, +.pf-c-form-control:hover ~ .login-form-input-label, +.pf-c-form-control[aria-invalid="true"] ~ .login-form-input-label { + display: block; +} + +.pf-c-form-control[aria-invalid="true"] ~ .login-form-input-label { + color: rgba(245, 46, 46, 1); +} +#kc-form-wrapper a { + font-family: "Swis721 Md BT"; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 16px; + color: rgba(189, 191, 191, 1); +} +#kc-registration { + width: 200px; +} +#kc-registration span { + font-family: "Swis721 Md BT"; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 16px; + color: #414142; + display: flex; + flex-wrap: wrap; + justify-content: flex-end; +} + +#kc-registration a { + padding-left: 8px; + font-family: "Swis721 Md BT"; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 16px; + color: #FFFFFF; +} +#kc-registration a::after { + content: " →"; +} + +#kc-info-wrapper { + padding: 0px; + background-color: #212121; +} +.register-form { + margin-bottom: 32px; +} + +.juice-footer { + position: absolute; + right: 56px; + bottom: 56px; + font-family: "Swis721 Th BT"; + font-style: normal; + font-weight: 300; + font-size: 12px; + line-height: 16px; + color: #BDBFBF; +} + +.juice-footer span { + font-family: "Swis721 Md BT"; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + color: #BDBFBF; +} + +/* password */ +#kc-form-options { + display: flex; + align-items: center; + width: 145px; +} + +#kc-form-options.signup-password { + width: 140px; +} + +#kc-form-options a { + font-family: "Swis721 Md BT"; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 16px; + color: #FFFFFF; +} + +.email-instruction { + padding-top: 12px; + padding-bottom: 32px; + font-family: "Swis721 Th BT"; + font-style: normal; + font-weight: 300; + font-size: 19px; + line-height: 28px; + display: flex; + align-items: center; + letter-spacing: -0.1px; + color: #98989E; +} + +/* sign-up */ + +.signup-terms { + margin: 0 auto; + width: 272px; + font-family: "Swis721 Th BT"; + font-style: normal; + font-weight: 300; + font-size: 12px; + line-height: 16px; + text-align: center; + color: #98989E; +} +.login-signup-terms { + margin: 32px auto; +} + +.form-horizontal .form-group { + margin-right: 0px; + margin-left: 0px; +} + +/* alert */ + +.pf-c-alert { + position: absolute; + bottom: 2.34%; + + width: 328px; + min-height: 68px; + background: rgba(0, 0, 0, 0.87); + box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.14), 0px 1px 18px rgba(0, 0, 0, 0.12), 0px 3px 5px rgba(0, 0, 0, 0.2); + border-radius: 4px; + z-index: 100; +} +.pf-c-alert.pf-m-inline { + display: flex; + justify-content: center; + align-items: center; + border: none; + margin-bottom: 0px; + padding: 0px; +} +.pf-c-alert__title { + width: 321px; + min-height: 68px; + padding-right: 8px; + padding-left: 15px; + font-family: "Swis721 Th BT"; + font-style: normal; + font-weight: 300; + font-size: 14px; + line-height: 20px; + color: #FFFFFF; +} +.pf-c-alert.pf-m-inline::before { + content: none; +} +.alert-button { + width: 65,79px; + height: 36px; + margin-right: 9px; + padding: 10px 9px 10px 30px; + font-family: "Roboto"; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 16px; + text-align: right; + letter-spacing: 1.25px; + text-transform: uppercase; + border: none; + outline: none; + background: linear-gradient(180deg, #FF9900 0%, #FF7100 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.welcome-container { + display: flex; + justify-content: space-between; + position: relative; +} + +#kc-current-locale-link { + position: relative; + font-family: "Swis721 Th BT"; + font-style: normal; + font-weight: 300; + font-size: 14px; + line-height: 20px; + color: #BDBFBF; +} +#kc-locale-dropdown a { + color: #BDBFBF; +} + +#kc-current-locale-link::before { + content: ''; + position: absolute; + background: url(../img/locale.svg); + width: 16px; + height: 16px; + top: 2px; + left: -17px; +} + +#kc-current-locale-link::after { + display: none; +} + +#kc-locale ul { + list-style: none; + top: 20px; + padding: 2px 0; + width: 136px; + background: rgba(23, 23, 23, 0.92); + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.32); + border-radius: 8px; + border: none; +} +.kc-dropdown-item { + position: relative; + height: 40px; + padding-left: 30px; + display: flex; + justify-content: flex-start; + align-items: center; +} +.kc-dropdown-item::before { + position: absolute; + width: 16px; + height: 12px; + content: ''; + left: 16px; +} +.kc-dropdown-item.English::before { + background: url(../img/english.svg); +} +.kc-dropdown-item.Deutsch::before { + background: url(../img/deutsch.svg); +} + +#kc-locale ul li a { + font-family: "Swis721 Th BT"; + font-style: normal; + font-weight: 300; + font-size: 14px; + line-height: 20px; + color: #BDBFBF !important; +} +#kc-locale .kc-dropdown-item.selected a { + font-family: "Swis721 Md BT"; +} + +#kc-locale ul li a:hover { + background: rgba(23, 23, 23, 0.92); + color: #FF7100 !important; +} +.kc-social-grid { + display: flex; + justify-content: center; + flex-wrap: nowrap; + grid-column-gap: 0px; +} +.social-button { + margin: 0 11px; + width: 48px !important; + height: 48px; + padding: 16px; + background: #212121; + border: none; + border-radius: 8px; +} +.social-button:last-child { + margin-right: 0; +} +.social-button:first-child { + margin-left: 0; +} +.separate-container { + margin: 32px 0px 32px; + display: flex; + align-items: center; +} +.separate-text { + margin: 0px 8px; + font-family: Swis721 Th BT; + font-style: normal; + font-weight: 300; + font-size: 16px; + line-height: 24px; + display: flex; + align-items: center; + text-align: center; + color: #98989E; +} +.separator { + flex: auto; + max-width: 109px; + min-width: 10px; + height: 1px; + background-color: #333333; +} + +.login-sign-image { + background: url(../img/apple.svg); + width: 16px; + height: 16px; + margin-right: 16px; +} + +.login-sign-image.apple { + background: url(../img/apple.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.login-sign-image.google { + background: url(../img/google.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.login-sign-image.facebook { + background: url(../img/facebook.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.login-sign-image.linkedin { + background: url(../img/linkedin.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.login-sign-image.twitter { + background: url(../img/twitter.svg); + background-size: contain; + background-repeat: no-repeat; +} +.kc-social-provider-logo { + font-size: 17px; + width: 13px; + height: 13px; + float: left; +} +.expired-image { + width: 96px; + height: 96px; + background: url(../img/clock.svg); + margin: 32px 0px 2px; +} +.expired-button { + margin-top: 32px; + text-align: center; + padding-top: 16px; + text-transform: none; +} +.expired-title { + width: 215px; +} +.login-pf a:hover { + text-decoration: none; + color: #BDBFBF; +} + +@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + .header-login-title { + background: #202020; + color: #FF7100; + } + .error-welcome { + background: #202020; + color: #F52E2E; + } + .alert-button { + background: #202020; + color: #FF7100; + } +} + +@media (max-height: 900px) and (max-width: 880px) { + .login-pf body::before { + display: none; + } + .juice-footer { + display: none; + } +} + +@media (max-height: 680px) { + .login-pf body { + align-items: unset; + } +} + +@media (max-width: 767px) { + #kc-locale { + position: relative !important; + width: inherit; + top: 0px; + right: 0px; + text-align: none; + } + + #kc-info-wrapper { + border-top: none; + } +} + +@media (min-width: 768px) { + .login-pf-page .login-pf-page-header { + margin-bottom: 0px; + } +} + + +@media (max-width: 440px) { + .login-pf body::before { + display: none; + } + + .login-pf body { + align-items: unset; + } + .login-pf-page .login-pf-header { + margin-bottom: 32px; + } + #kc-header { + display: none; + } + .card-login { + min-width: 328px; + max-width: 448px; + padding: 16px 10px; + box-shadow: none; + } + .juice-footer { + display: none; + } +} + +@media (max-height: 440px) { + .login-pf body::before { + display: none; + } + .juice-footer { + display: none; + } +} + +@media (max-width: 360px) { + .card-login { + width: 360px; + padding: 16px 10px; + box-shadow: none; + } +} + +@media (max-width: 359px) { + .card-login { + min-width: 248px; + max-width: 304px; + padding: 16px 10px; + box-shadow: none; + } + #kc-registration { + width: 155px; + } + .social-button { + padding: 16px 12px; + } +} + +@media (max-width: 300px) { + .card-login { + min-width: 238px; + max-width: 278px; + padding: 16px 10px; + box-shadow: none; + } + .social-button { + padding: 16px 10px; + } +} + +@media (max-width: 279px) { + .login-pf-page { + overflow-y: auto; + overflow-x: auto; + } +} \ No newline at end of file diff --git a/juice/login/resources/fonts/Roboto-Medium.ttf b/juice/login/resources/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..f714a51 Binary files /dev/null and b/juice/login/resources/fonts/Roboto-Medium.ttf differ diff --git a/juice/login/resources/fonts/Swiss721MediumBT.ttf b/juice/login/resources/fonts/Swiss721MediumBT.ttf new file mode 100644 index 0000000..31dfb5c Binary files /dev/null and b/juice/login/resources/fonts/Swiss721MediumBT.ttf differ diff --git a/juice/login/resources/fonts/Swiss721ThinBT.ttf b/juice/login/resources/fonts/Swiss721ThinBT.ttf new file mode 100644 index 0000000..ebc5cf5 Binary files /dev/null and b/juice/login/resources/fonts/Swiss721ThinBT.ttf differ diff --git a/juice/login/resources/img/apple.svg b/juice/login/resources/img/apple.svg new file mode 100644 index 0000000..3376f9f --- /dev/null +++ b/juice/login/resources/img/apple.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/login/resources/img/clock.svg b/juice/login/resources/img/clock.svg new file mode 100644 index 0000000..4abc291 --- /dev/null +++ b/juice/login/resources/img/clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/login/resources/img/deutsch.svg b/juice/login/resources/img/deutsch.svg new file mode 100644 index 0000000..9cb0d95 --- /dev/null +++ b/juice/login/resources/img/deutsch.svg @@ -0,0 +1,9 @@ + + + + Flag of Germany + + + + diff --git a/juice/login/resources/img/english.svg b/juice/login/resources/img/english.svg new file mode 100644 index 0000000..75a198d --- /dev/null +++ b/juice/login/resources/img/english.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/juice/login/resources/img/error.svg b/juice/login/resources/img/error.svg new file mode 100644 index 0000000..1970e12 --- /dev/null +++ b/juice/login/resources/img/error.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/login/resources/img/facebook.svg b/juice/login/resources/img/facebook.svg new file mode 100644 index 0000000..f4ba330 --- /dev/null +++ b/juice/login/resources/img/facebook.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/login/resources/img/google.svg b/juice/login/resources/img/google.svg new file mode 100644 index 0000000..8649a02 --- /dev/null +++ b/juice/login/resources/img/google.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/juice/login/resources/img/juice-bc copy.svg b/juice/login/resources/img/juice-bc copy.svg new file mode 100644 index 0000000..3c65812 --- /dev/null +++ b/juice/login/resources/img/juice-bc copy.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/juice/login/resources/img/juice-bc.svg b/juice/login/resources/img/juice-bc.svg new file mode 100644 index 0000000..3c65812 --- /dev/null +++ b/juice/login/resources/img/juice-bc.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/juice/login/resources/img/juice.svg b/juice/login/resources/img/juice.svg new file mode 100644 index 0000000..ee1ec0d --- /dev/null +++ b/juice/login/resources/img/juice.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/juice/login/resources/img/linkedin.svg b/juice/login/resources/img/linkedin.svg new file mode 100644 index 0000000..c87b46e --- /dev/null +++ b/juice/login/resources/img/linkedin.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/login/resources/img/locale.svg b/juice/login/resources/img/locale.svg new file mode 100644 index 0000000..79484b7 --- /dev/null +++ b/juice/login/resources/img/locale.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/login/resources/img/twitter.svg b/juice/login/resources/img/twitter.svg new file mode 100644 index 0000000..3727d76 --- /dev/null +++ b/juice/login/resources/img/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/login/resources/img/verify-email.svg b/juice/login/resources/img/verify-email.svg new file mode 100644 index 0000000..0ff2735 --- /dev/null +++ b/juice/login/resources/img/verify-email.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/login/resources/js/script.js b/juice/login/resources/js/script.js new file mode 100644 index 0000000..1c235cf --- /dev/null +++ b/juice/login/resources/js/script.js @@ -0,0 +1,5 @@ + +function hideSnackbar() { + var x = document.getElementById("snackbar"); + document.getElementById('snackbar').style.display = 'none'; +} \ No newline at end of file diff --git a/juice/login/template.ftl b/juice/login/template.ftl new file mode 100644 index 0000000..904136a --- /dev/null +++ b/juice/login/template.ftl @@ -0,0 +1,134 @@ +<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false showAnotherWayIfPresent=true> + + + + + + + + + <#if properties.meta?has_content> + <#list properties.meta?split(' ') as meta> + + + + ${msg("loginTitle",(realm.displayName!''))} + + <#if properties.stylesCommon?has_content> + <#list properties.stylesCommon?split(' ') as style> + + + + <#if properties.styles?has_content> + <#list properties.styles?split(' ') as style> + + + + <#if properties.scripts?has_content> + <#list properties.scripts?split(' ') as script> + + + + <#if scripts??> + <#list scripts as script> + + + + + + +
+
+
+
+
+
+ + <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())> + <#if displayRequiredFields> +
+
+ * ${msg("requiredFields")} +
+
+

<#nested "header">

+
+
+ <#else> +

<#nested "header">

+ + <#else> + <#if displayRequiredFields> +
+
+ * ${msg("requiredFields")} +
+
+ <#nested "show-username"> +
+ + + + +
+
+
+ <#else> + <#nested "show-username"> +
+ + + + +
+ + +
+
+
+ + <#-- App-initiated actions should not see warning messages about the need to complete the action --> + <#-- during login. --> + + + <#nested "form"> + + <#if auth?has_content && auth.showTryAnotherWayLink() && showAnotherWayIfPresent> +
+ +
+ + + <#if displayInfo> +
+
+ <#nested "info"> +
+
+ +
+
+ +
+
+ <#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)> +
+ +
${kcSanitize(message.summary)?no_esc}
+ +
+ + + + diff --git a/juice/login/theme.properties b/juice/login/theme.properties new file mode 100644 index 0000000..68bb1a9 --- /dev/null +++ b/juice/login/theme.properties @@ -0,0 +1,8 @@ +parent=keycloak +import=common/keycloak + +styles=css/login.css css/styles.css + +scripts=js/script.js + +kcFormCardClass=card-login \ No newline at end of file diff --git a/juice/welcome/index.ftl b/juice/welcome/index.ftl new file mode 100644 index 0000000..2999b2e --- /dev/null +++ b/juice/welcome/index.ftl @@ -0,0 +1,68 @@ + +  + + + Welcome to ${productNameFull} + + + + + + + + + <#if properties.stylesCommon?has_content> + <#list properties.stylesCommon?split(' ') as style> + + + + <#if properties.styles?has_content> + <#list properties.styles?split(' ') as style> + + + + + + + +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/juice/welcome/messages/messages_en.properties b/juice/welcome/messages/messages_en.properties new file mode 100644 index 0000000..c5e2d19 --- /dev/null +++ b/juice/welcome/messages/messages_en.properties @@ -0,0 +1,20 @@ +loginAccountTitle=Sign in + +doRegister=Sign up +doForgotPassword=Forgot password? +noAccount=Not a member? + +emailForgotTitle=Reset +emailForgotTitle1=your password +emailInstruction=Please enter your email address. +emailInstruction1=We will send you an email to reset your password. +doSubmit=Send email +backToLogin=Go back +haveAccount=Have an account? + +registerTitle=Join +registerTitle1=The Community + +emailVerifyTitle=Verify your email +emailVerifyInstruction1=You need to verify your email address to activate your account. An email with instructions to verify your email address has been sent to you +emailVerifyInstruction3=Re-send the email \ No newline at end of file diff --git a/juice/welcome/messages/messages_ru.properties b/juice/welcome/messages/messages_ru.properties new file mode 100644 index 0000000..c5e2d19 --- /dev/null +++ b/juice/welcome/messages/messages_ru.properties @@ -0,0 +1,20 @@ +loginAccountTitle=Sign in + +doRegister=Sign up +doForgotPassword=Forgot password? +noAccount=Not a member? + +emailForgotTitle=Reset +emailForgotTitle1=your password +emailInstruction=Please enter your email address. +emailInstruction1=We will send you an email to reset your password. +doSubmit=Send email +backToLogin=Go back +haveAccount=Have an account? + +registerTitle=Join +registerTitle1=The Community + +emailVerifyTitle=Verify your email +emailVerifyInstruction1=You need to verify your email address to activate your account. An email with instructions to verify your email address has been sent to you +emailVerifyInstruction3=Re-send the email \ No newline at end of file diff --git a/juice/welcome/resources/apple.svg b/juice/welcome/resources/apple.svg new file mode 100644 index 0000000..fe11cc5 --- /dev/null +++ b/juice/welcome/resources/apple.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/welcome/resources/css/styles.css b/juice/welcome/resources/css/styles.css new file mode 100644 index 0000000..d32ffef --- /dev/null +++ b/juice/welcome/resources/css/styles.css @@ -0,0 +1,330 @@ +.login-pf { + height: 100%; + background: none; +} + +body { + background: url(../juice-bc.svg); + background-repeat: no-repeat; + background-color: #202020; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +body::before { + position: absolute; + width: 56px; + height: 56px; + content: ""; + top: 56px; + left: 63px; + background: url(../juice.svg); + background-repeat: no-repeat; +} + +.login-pf-page { + padding-top: 0px; +} + +.h1, .h2, .h3, h1, h2, h3 { + margin-top: 0px; + margin-bottom: 0px; +} + +#kc-header-wrapper { + padding: 0px 10px; +} + +.card-login { + margin: 0 auto; + box-shadow: -8px -8px 32px rgba(101, 101, 101, 0.12), -2px -2px 16px rgba(101, 101, 101, 0.24), 4px 4px 16px rgba(0, 0, 0, 0.3), 8px 8px 32px rgba(0, 0, 0, 0.1); + padding: 56px; + min-width: 440px; + max-width: 440px; + border-radius: 16px; + background-color: #212121; +} + +.group-form { + padding: 32px 0px; +} + +.login-sign-button { + display: flex; + justify-content: center; + align-items: center; + height: 56px; + margin-bottom: 24px; + background: #212121; + box-shadow: -2px -2px 4px rgba(101, 101, 101, 0.12), -1px -1px 3px rgba(101, 101, 101, 0.24), 1px 1px 2px rgba(0, 0, 0, 0.3), 2px 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 4px; + font-family: Swis721 Th BT; + font-style: normal; + font-weight: 300; + font-size: 16px; + line-height: 24px; + text-align: center; + color: #BDBFBF; + cursor: pointer; +} +.login-sign-button:hover, +.login-sign-button:focus { + background: #292929; +} + +.login-sign-image { + background: url(../apple.svg); + width: 24px; + height: 24px; + margin-right: 16px; +} + +.login-sign-image.apple { + background: url(../apple.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.login-sign-image.google { + background: url(../google.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.login-sign-image.facebook { + background: url(../facebook.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.login-sign-image.linkedin { + background: url(../linkedin.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.login-sign-image.twitter { + background: url(../twitter.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.login-social-sign-container { + display: flex; + justify-content: space-between; +} + +.login-social-sign-button { + display: flex; + justify-content: center; + align-items: center; + width: 156px; + height: 56px; + background: #212121; + border: 1px solid #292929; + box-sizing: border-box; + border-radius: 4px; + font-family: Swis721 Th BT; + font-style: normal; + font-weight: 300; + font-size: 16px; + line-height: 24px; + text-align: center; + color: #BDBFBF; + cursor: pointer; +} +.login-social-sign-button:hover, +.login-social-sign-button:focus { + background: #292929; +} +.large-social-sign-button:hover { + color: #BDBFBF; + text-decoration: none; +} + +.login-sign-link { + padding-left: 3px; + font-weight: 500; +} + +.separate-text { + font-family: Swis721 Th BT; + font-style: normal; + font-weight: 300; + font-size: 16px; + line-height: 24px; + display: flex; + align-items: center; + text-align: center; + color: #98989E; +} + +.separate-text::before { + width: 147px; + height: 1px; + margin-right: 8px; + content: ""; + background-color: #333333; +} +.separate-text::after { + width: 147px; + height: 1px; + margin-left: 8px; + content: ""; + background-color: #333333; +} + +.large-social-sign-button { + width: 328px; +} + +.signup-terms-container { + display: flex; + justify-content: center; +} + +.login-pf-page-header { + background: url(../img/juice.svg); + background-position-x: 63px; + background-position-y: 56px; + background-repeat: no-repeat; +} +.header-login-welcome { + font-family: Swis721 Th BT; + font-style: normal; + font-weight: 300; + font-size: 14px; + line-height: 20px; + color: #98989E; +} +.header-login-title { + padding-top: 4px; + font-family: Swis721 Th BT; + font-style: normal; + font-weight: 300; + font-size: 38px; + line-height: 48px; + letter-spacing: -0.75px; + background: linear-gradient(180deg, #FF9900 0%, #FF7100 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +#kc-page-title{ + text-align: left; +} + +#kc-form-buttons { + margin-top: 32px; + margin-bottom: 32px; +} + +.pf-c-button { + height: 56px; + background: #181818; + box-shadow: -4px -4px 8px rgba(101, 101, 101, 0.08), -1px -1px 3px rgba(101, 101, 101, 0.12), 1px 1px 4px rgba(0, 0, 0, 0.3), 4px 4px 8px rgba(0, 0, 0, 0.4); + border-radius: 4px; + font-family: Swis721 Md BT; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + text-align: center; + color: #BDBFBF; +} + +.pf-c-form-control{ + position: relative; + min-height: 56px; + border: 1px solid #333333; + box-sizing: border-box; + border-radius: 4px; + background: #212121; + font-family: Swis721 Th BT; + padding-left: 16px; + font-style: normal; + font-weight: 300; + font-size: 16px; + line-height: 24px; + color: #757575; +} + +.form-group { + position: relative; + margin-bottom: 24px; +} + +#input-error { + margin-top: 8px; + font-family: Swis721 Th BT; + font-style: normal; + font-weight: 300; + font-size: 14px; + line-height: 20px; + color: #BDBFBF; +} + + +.pf-c-form-control:focus, .pf-c-form-control:hover{ + outline: none; + border: 1px solid rgba(255, 113, 0, 1); + caret-color: rgba(255, 113, 0, 1); + /* font-family: Swis721 Th BT; */ +} +.pf-c-form-control[aria-invalid="true"] { + border: 1px solid rgba(245, 46, 46, 1); + background: #212121; +} +.login-form-input-label { + position: absolute; + display: none; + padding: 0px 4px; + top: -10px; + left: 12px; + color:rgba(255, 113, 0, 1); + z-index: 100; + background-color: #212121; +} +.pf-c-form-control:focus ~ .login-form-input-label, +.pf-c-form-control:hover ~ .login-form-input-label, +.pf-c-form-control[aria-invalid="true"] ~ .login-form-input-label { + display: block; +} + +.pf-c-form-control[aria-invalid="true"] ~ .login-form-input-label { + color: rgba(245, 46, 46, 1); +} + +.juice-footer { + position: absolute; + right: 56px; + bottom: 56px; + font-family: Swis721 Th BT; + font-style: normal; + font-weight: 300; + font-size: 12px; + line-height: 16px; + color: #BDBFBF; +} + +.juice-footer span { + font-family: Swis721 Md BT; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + color: #BDBFBF; +} + +.signup-terms { + width: 290px; + font-family: Swis721 Th BT; + font-style: normal; + font-weight: 300; + font-size: 12px; + line-height: 16px; + text-align: center; + color: #BDBFBF; +} diff --git a/juice/welcome/resources/facebook.svg b/juice/welcome/resources/facebook.svg new file mode 100644 index 0000000..0c95dd9 --- /dev/null +++ b/juice/welcome/resources/facebook.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/welcome/resources/google.svg b/juice/welcome/resources/google.svg new file mode 100644 index 0000000..c3ac925 --- /dev/null +++ b/juice/welcome/resources/google.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/juice/welcome/resources/js/realm.js b/juice/welcome/resources/js/realm.js new file mode 100644 index 0000000..fad9d27 --- /dev/null +++ b/juice/welcome/resources/js/realm.js @@ -0,0 +1,3219 @@ +function getAccess(Auth, Current, role) { + if (!Current.realm)return false; + var realmAccess = Auth.user && Auth.user['realm_access']; + if (realmAccess) { + realmAccess = realmAccess[Current.realm.realm]; + if (realmAccess) { + return realmAccess.indexOf(role) >= 0; + } + } + return false; +} + +function getAccessObject(Auth, Current) { + return { + get createRealm() { + return Auth.user && Auth.user.createRealm; + }, + + get queryUsers() { + return getAccess(Auth, Current, 'query-users') || this.viewUsers; + }, + + get queryGroups() { + return getAccess(Auth, Current, 'query-groups') || this.viewUsers; + }, + + get queryClients() { + return getAccess(Auth, Current, 'query-clients') || this.viewClients; + }, + + get viewRealm() { + return getAccess(Auth, Current, 'view-realm') || getAccess(Auth, Current, 'manage-realm') || this.manageRealm; + }, + + get viewClients() { + return getAccess(Auth, Current, 'view-clients') || getAccess(Auth, Current, 'manage-clients') || this.manageClients; + }, + + get viewUsers() { + return getAccess(Auth, Current, 'view-users') || getAccess(Auth, Current, 'manage-users') || this.manageClients; + }, + + get viewEvents() { + return getAccess(Auth, Current, 'view-events') || getAccess(Auth, Current, 'manage-events') || this.manageClients; + }, + + get viewIdentityProviders() { + return getAccess(Auth, Current, 'view-identity-providers') || getAccess(Auth, Current, 'manage-identity-providers') || this.manageIdentityProviders; + }, + + get viewAuthorization() { + return getAccess(Auth, Current, 'view-authorization') || this.manageAuthorization; + }, + + get manageRealm() { + return getAccess(Auth, Current, 'manage-realm'); + }, + + get manageClients() { + return getAccess(Auth, Current, 'manage-clients'); + }, + + get manageUsers() { + return getAccess(Auth, Current, 'manage-users'); + }, + + get manageEvents() { + return getAccess(Auth, Current, 'manage-events'); + }, + + get manageIdentityProviders() { + return getAccess(Auth, Current, 'manage-identity-providers'); + }, + + get manageAuthorization() { + return getAccess(Auth, Current, 'manage-authorization'); + }, + + get impersonation() { + return getAccess(Auth, Current, 'impersonation'); + } + }; +} + + +module.controller('GlobalCtrl', function($scope, $http, Auth, Current, $location, Notifications, ServerInfo, RealmSpecificLocalizationTexts) { + $scope.authUrl = authUrl; + $scope.resourceUrl = resourceUrl; + $scope.auth = Auth; + $scope.serverInfo = ServerInfo.get(); + + $scope.access = getAccessObject(Auth, Current); + + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.fragment = $location.path(); + $scope.path = $location.path().substring(1).split("/"); + }); + + $scope.$watch(function() { + return Current.realm; + }, function() { + if(Current.realm !== null && currentRealm !== Current.realm.id) { + currentRealm = Current.realm.id; + translateProvider.translations(locale, resourceBundle); + RealmSpecificLocalizationTexts.get({id: currentRealm, locale: locale}, function (localizationTexts) { + translateProvider.translations(locale, localizationTexts.toJSON()); + }) + } + }) +}); + +module.controller('HomeCtrl', function(Realm, Auth, Current, $location) { + + Realm.query(null, function(realms) { + var realm; + if (realms.length == 1) { + realm = realms[0]; + } else if (realms.length == 2) { + if (realms[0].realm == Auth.user.realm) { + realm = realms[1]; + } else if (realms[1].realm == Auth.user.realm) { + realm = realms[0]; + } + } + if (realm) { + Current.realms = realms; + Current.realm = realm; + var access = getAccessObject(Auth, Current); + if (access.viewRealm || access.manageRealm) { + $location.url('/realms/' + realm.realm ); + } else if (access.queryClients) { + $location.url('/realms/' + realm.realm + "/clients"); + } else if (access.viewIdentityProviders) { + $location.url('/realms/' + realm.realm + "/identity-provider-settings"); + } else if (access.queryUsers) { + $location.url('/realms/' + realm.realm + "/users"); + } else if (access.queryGroups) { + $location.url('/realms/' + realm.realm + "/groups"); + } else if (access.viewEvents) { + $location.url('/realms/' + realm.realm + "/events"); + } + } else { + $location.url('/realms'); + } + }); +}); + +module.controller('RealmTabCtrl', function(Dialog, $scope, Current, Realm, Notifications, $location) { + $scope.removeRealm = function() { + Dialog.confirmDelete(Current.realm.realm, 'realm', function() { + Realm.remove({ id : Current.realm.realm }, function() { + Current.realms = Realm.query(); + Notifications.success("The realm has been deleted."); + $location.url("/"); + }); + }); + }; +}); + +module.controller('ServerInfoCtrl', function($scope, ServerInfo) { + ServerInfo.reload(); + + $scope.serverInfo = ServerInfo.get(); + + $scope.$watch($scope.serverInfo, function() { + $scope.providers = []; + for(var spi in $scope.serverInfo.providers) { + var p = angular.copy($scope.serverInfo.providers[spi]); + p.name = spi; + $scope.providers.push(p) + } + }); + + $scope.serverInfoReload = function() { + ServerInfo.reload(); + } +}); + +module.controller('RealmListCtrl', function($scope, Realm, Current) { + $scope.realms = Realm.query(); + Current.realms = $scope.realms; +}); + +module.controller('RealmDropdownCtrl', function($scope, Realm, Current, Auth, $location) { +// Current.realms = Realm.get(); + $scope.current = Current; + + $scope.changeRealm = function(selectedRealm) { + $location.url("/realms/" + selectedRealm); + } +}); + +module.controller('RealmCreateCtrl', function($scope, Current, Realm, $upload, $http, $location, $route, Dialog, Notifications, Auth, $modal) { + console.log('RealmCreateCtrl'); + + Current.realm = null; + + $scope.realm = { + enabled: true + }; + + $scope.changed = false; + $scope.files = []; + + var oldCopy = angular.copy($scope.realm); + + $scope.importFile = function($fileContent){ + $scope.realm = angular.copy(JSON.parse($fileContent)); + $scope.importing = true; + }; + + $scope.viewImportDetails = function() { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/view-object.html', + controller: 'ObjectModalCtrl', + resolve: { + object: function () { + return $scope.realm; + } + } + }) + }; + + $scope.$watch('realm', function() { + if (!angular.equals($scope.realm, oldCopy)) { + $scope.changed = true; + } + }, true); + + $scope.$watch('realm.realm', function() { + $scope.realm.id = $scope.realm.realm; + }, true); + + $scope.save = function() { + var realmCopy = angular.copy($scope.realm); + Realm.create(realmCopy, function() { + Notifications.success("The realm has been created."); + + Auth.refreshPermissions(function() { + $scope.$apply(function() { + $location.url("/realms/" + realmCopy.realm); + }); + }); + }); + }; + + $scope.cancel = function() { + $location.url("/"); + }; + + $scope.reset = function() { + $route.reload(); + } +}); + +module.controller('ObjectModalCtrl', function($scope, object) { + $scope.object = object; +}); + +module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, $window, Dialog, Notifications, Auth) { + $scope.createRealm = !realm.realm; + $scope.serverInfo = serverInfo; + $scope.realmName = realm.realm; + $scope.disableRename = realm.realm == masterRealm; + $scope.authServerUrl = authServerUrl; + + if (Current.realm == null || Current.realm.realm != realm.realm) { + for (var i = 0; i < Current.realms.length; i++) { + if (realm.realm == Current.realms[i].realm) { + Current.realm = Current.realms[i]; + break; + } + } + } + for (var i = 0; i < Current.realms.length; i++) { + if (Current.realms[i].realm == realm.realm) { + Current.realm = Current.realms[i]; + } + } + $scope.realm = angular.copy(realm); + + var oldCopy = angular.copy($scope.realm); + + $scope.changed = $scope.create; + + $scope.$watch('realm', function() { + if (!angular.equals($scope.realm, oldCopy)) { + $scope.changed = true; + } + }, true); + $scope.$watch('realmName', function() { + if (!angular.equals($scope.realmName, oldCopy.realm)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + var realmCopy = angular.copy($scope.realm); + realmCopy.realm = $scope.realmName; + $scope.changed = false; + var nameChanged = !angular.equals($scope.realmName, oldCopy.realm); + var oldName = oldCopy.realm; + Realm.update({ id : oldCopy.realm}, realmCopy, function () { + var data = Realm.query(function () { + Current.realms = data; + for (var i = 0; i < Current.realms.length; i++) { + if (Current.realms[i].realm == realmCopy.realm) { + Current.realm = Current.realms[i]; + oldCopy = angular.copy($scope.realm); + } + } + }); + + if (nameChanged) { + console.debug(Auth); + console.debug(Auth.authz.tokenParsed.iss); + + if (Auth.authz.tokenParsed.iss.endsWith(masterRealm)) { + Auth.refreshPermissions(function () { + Auth.refreshPermissions(function () { + Notifications.success("Your changes have been saved to the realm."); + $scope.$apply(function () { + $location.url("/realms/" + realmCopy.realm); + }); + }); + }); + } else { + delete Auth.authz.token; + delete Auth.authz.refreshToken; + + var newLocation = $window.location.href.replace('/' + oldName + '/', '/' + realmCopy.realm + '/') + .replace('/realms/' + oldName, '/realms/' + realmCopy.realm); + window.location.replace(newLocation); + } + } else { + $location.url("/realms/" + realmCopy.realm); + Notifications.success("Your changes have been saved to the realm."); + } + }); + }; + + $scope.reset = function() { + $scope.realm = angular.copy(oldCopy); + $scope.changed = false; + }; + + $scope.cancel = function() { + window.history.back(); + }; +}); + +function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, url) { + $scope.realm = angular.copy(realm); + $scope.serverInfo = serverInfo; + $scope.registrationAllowed = $scope.realm.registrationAllowed; + + var oldCopy = angular.copy($scope.realm); + + $scope.changed = false; + + $scope.$watch('realm', function() { + if (!angular.equals($scope.realm, oldCopy)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + var realmCopy = angular.copy($scope.realm); + console.log('updating realm...'); + $scope.changed = false; + console.log('oldCopy.realm - ' + oldCopy.realm); + Realm.update({ id : oldCopy.realm}, realmCopy, function () { + $route.reload(); + Notifications.success("Your changes have been saved to the realm."); + $scope.registrationAllowed = $scope.realm.registrationAllowed; + }); + }; + + $scope.reset = function() { + $scope.realm = angular.copy(oldCopy); + $scope.changed = false; + }; + + $scope.cancel = function() { + $route.reload(); + }; + +} + +module.controller('DefenseHeadersCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) { + genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/defense/headers"); +}); + +module.controller('RealmLoginSettingsCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) { + // KEYCLOAK-5474: Make sure duplicateEmailsAllowed is disabled if loginWithEmailAllowed + $scope.$watch('realm.loginWithEmailAllowed', function() { + if ($scope.realm.loginWithEmailAllowed) { + $scope.realm.duplicateEmailsAllowed = false; + } + }); + + genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/login-settings"); +}); + +module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) { + $scope.optionsDigits = [ 6, 8 ]; + + genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/otp-policy"); +}); + +module.controller('RealmWebAuthnPolicyCtrl', function ($scope, Current, Realm, realm, serverInfo, $http, $route, $location, Dialog, Notifications) { + + $scope.deleteAcceptableAaguid = function(index) { + $scope.realm.webAuthnPolicyAcceptableAaguids.splice(index, 1); + }; + + $scope.addAcceptableAaguid = function() { + $scope.realm.webAuthnPolicyAcceptableAaguids.push($scope.newAcceptableAaguid); + $scope.newAcceptableAaguid = ""; + }; + + // Just for case the user fill particular URL with disabled WebAuthn feature. + $scope.redirectIfWebAuthnDisabled = function () { + if (!serverInfo.featureEnabled('WEB_AUTHN')) { + $location.url("/realms/" + $scope.realm.realm + "/authentication"); + } + }; + + genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/webauthn-policy"); +}); + +module.controller('RealmWebAuthnPasswordlessPolicyCtrl', function ($scope, Current, Realm, realm, serverInfo, $http, $route, $location, Dialog, Notifications) { + + $scope.deleteAcceptableAaguid = function(index) { + $scope.realm.webAuthnPolicyPasswordlessAcceptableAaguids.splice(index, 1); + }; + + $scope.addAcceptableAaguid = function() { + $scope.realm.webAuthnPolicyPasswordlessAcceptableAaguids.push($scope.newAcceptableAaguid); + $scope.newAcceptableAaguid = ""; + }; + + // Just for case the user fill particular URL with disabled WebAuthn feature. + $scope.redirectIfWebAuthnDisabled = function () { + if (!serverInfo.featureEnabled('WEB_AUTHN')) { + $location.url("/realms/" + $scope.realm.realm + "/authentication"); + } + }; + + genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/webauthn-policy-passwordless"); +}); + +module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) { + genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings"); + + $scope.supportedLocalesOptions = { + 'multiple' : true, + 'simple_tags' : true, + 'tags' : [] + }; + + updateSupported(); + + function localeForTheme(type, name) { + name = name || 'base'; + for (var i = 0; i < serverInfo.themes[type].length; i++) { + if (serverInfo.themes[type][i].name == name) { + return serverInfo.themes[type][i].locales || []; + } + } + return []; + } + + function updateSupported() { + if ($scope.realm.internationalizationEnabled) { + var accountLocales = localeForTheme('account', $scope.realm.accountTheme); + var loginLocales = localeForTheme('login', $scope.realm.loginTheme); + var emailLocales = localeForTheme('email', $scope.realm.emailTheme); + + var supportedLocales = []; + for (var i = 0; i < accountLocales.length; i++) { + var l = accountLocales[i]; + if (loginLocales.indexOf(l) >= 0 && emailLocales.indexOf(l) >= 0) { + supportedLocales.push(l); + } + } + + $scope.supportedLocalesOptions.tags = supportedLocales; + + if (!$scope.realm.supportedLocales) { + $scope.realm.supportedLocales = supportedLocales; + } else { + for (var i = 0; i < $scope.realm.supportedLocales.length; i++) { + if (supportedLocales.indexOf($scope.realm.supportedLocales[i]) == -1) { + $scope.realm.supportedLocales = supportedLocales; + } + } + } + + if (!$scope.realm.defaultLocale || supportedLocales.indexOf($scope.realm.defaultLocale) == -1) { + $scope.realm.defaultLocale = 'en'; + } + } + } + + $scope.$watch('realm.loginTheme', updateSupported); + $scope.$watch('realm.accountTheme', updateSupported); + $scope.$watch('realm.emailTheme', updateSupported); + $scope.$watch('realm.internationalizationEnabled', updateSupported); +}); + +module.controller('RealmLocalizationCtrl', function($scope, Current, $location, Realm, realm, serverInfo, Notifications, RealmSpecificLocales, realmSpecificLocales, RealmSpecificLocalizationTexts, RealmSpecificLocalizationText, Dialog, $translate){ + $scope.realm = realm; + $scope.realmSpecificLocales = realmSpecificLocales; + $scope.newLocale = null; + $scope.selectedRealmSpecificLocales = null; + $scope.localizationTexts = null; + + $scope.createLocale = function() { + if(!$scope.newLocale) { + Notifications.error($translate.instant('missing-locale')); + return; + } + $scope.realmSpecificLocales.push($scope.newLocale) + $scope.selectedRealmSpecificLocales = $scope.newLocale; + $scope.newLocale = null; + $location.url('/create/localization/' + realm.realm + '/' + $scope.selectedRealmSpecificLocales); + } + + $scope.$watch(function() { + return $scope.selectedRealmSpecificLocales; + }, function() { + if($scope.selectedRealmSpecificLocales != null) { + $scope.updateRealmSpecificLocalizationTexts(); + } + }) + + $scope.updateRealmSpecificLocales = function() { + RealmSpecificLocales.get({id: realm.realm}, function (updated) { + $scope.realmSpecificLocales = updated; + }) + } + + $scope.updateRealmSpecificLocalizationTexts = function() { + RealmSpecificLocalizationTexts.get({id: realm.realm, locale: $scope.selectedRealmSpecificLocales }, function (updated) { + $scope.localizationTexts = updated; + }) + } + + $scope.removeLocalizationText = function(key) { + Dialog.confirmDelete(key, 'localization text', function() { + RealmSpecificLocalizationText.remove({ + realm: realm.realm, + locale: $scope.selectedRealmSpecificLocales, + key: key + }, function () { + $scope.updateRealmSpecificLocalizationTexts(); + Notifications.success($translate.instant('localization-text.remove.success')); + }); + }); + } +}); + +module.controller('RealmLocalizationUploadCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, $upload, $translate){ + $scope.realm = realm; + $scope.locale = null; + $scope.files = []; + + $scope.onFileSelect = function($files) { + $scope.files = $files; + }; + + $scope.reset = function() { + $scope.locale = null; + $scope.files = null; + }; + + $scope.save = function() { + + if(!$scope.files || $scope.files.length === 0) { + Notifications.error($translate.instant('missing-file')); + return; + } + //$files: an array of files selected, each file has name, size, and type. + for (var i = 0; i < $scope.files.length; i++) { + var $file = $scope.files[i]; + $scope.upload = $upload.upload({ + url: authUrl + '/admin/realms/' + realm.realm + '/localization/' + $scope.locale, + file: $file + }).then(function(response) { + $scope.reset(); + Notifications.success($translate.instant('localization-file.upload.success')); + }).catch(function() { + Notifications.error($translate.instant('localization-file.upload.error')); + }); + } + }; + +}); + +module.controller('RealmLocalizationDetailCtrl', function($scope, Current, $location, Realm, realm, Notifications, locale, key, RealmSpecificLocalizationText, localizationText, $translate){ + $scope.realm = realm; + $scope.locale = locale; + $scope.key = key; + $scope.value = ((localizationText)? localizationText.content : null); + + $scope.create = !key; + + $scope.save = function() { + if ($scope.create) { + RealmSpecificLocalizationText.save({ + realm: realm.realm, + locale: $scope.locale, + key: $scope.key + }, $scope.value, function (data, headers) { + $location.url("/realms/" + realm.realm + "/localization"); + Notifications.success($translate.instant('localization-text.create.success')); + }); + } else { + RealmSpecificLocalizationText.save({ + realm: realm.realm, + locale: $scope.locale, + key: $scope.key + }, $scope.value, function (data, headers) { + $location.url("/realms/" + realm.realm + "/localization"); + Notifications.success($translate.instant('localization-text.update.success')); + }); + } + }; + + $scope.cancel = function () { + $location.url("/realms/" + realm.realm + "/localization"); + }; + +}); + +module.controller('RealmCacheCtrl', function($scope, realm, RealmClearUserCache, RealmClearRealmCache, RealmClearKeysCache, Notifications) { + $scope.realm = angular.copy(realm); + + $scope.clearUserCache = function() { + RealmClearUserCache.save({ realm: realm.realm}, function () { + Notifications.success("User cache cleared"); + }); + } + + $scope.clearRealmCache = function() { + RealmClearRealmCache.save({ realm: realm.realm}, function () { + Notifications.success("Realm cache cleared"); + }); + } + + $scope.clearKeysCache = function() { + RealmClearKeysCache.save({ realm: realm.realm}, function () { + Notifications.success("Public keys cache cleared"); + }); + } + + +}); + +module.controller('RealmPasswordPolicyCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, serverInfo) { + var parse = function(policyString) { + var policies = []; + if (!policyString || policyString.length == 0){ + return policies; + } + + var policyArray = policyString.split(" and "); + + for (var i = 0; i < policyArray.length; i ++){ + var policyToken = policyArray[i]; + var id; + var value; + if (policyToken.indexOf('(') == -1) { + id = policyToken.trim(); + value = null; + } else { + id = policyToken.substring(0, policyToken.indexOf('(')); + value = policyToken.substring(policyToken.indexOf('(') + 1, policyToken.lastIndexOf(')')).trim(); + } + + for (var j = 0; j < serverInfo.passwordPolicies.length; j++) { + if (serverInfo.passwordPolicies[j].id == id) { + // clone + var p = JSON.parse(JSON.stringify(serverInfo.passwordPolicies[j])); + + p.value = value && value || p.defaultValue; + policies.push(p); + } + } + } + return policies; + }; + + var toString = function(policies) { + if (!policies || policies.length == 0) { + return ""; + } + var policyString = ""; + for (var i = 0; i < policies.length; i++) { + policyString += policies[i].id + '(' + policies[i].value + ')'; + if (i != policies.length - 1) { + policyString += ' and '; + } + } + return policyString; + } + + $scope.realm = realm; + $scope.serverInfo = serverInfo; + + $scope.changed = false; + console.log(JSON.stringify(parse(realm.passwordPolicy))); + $scope.policy = parse(realm.passwordPolicy); + var oldCopy = angular.copy($scope.policy); + + $scope.$watch('policy', function() { + $scope.changed = ! angular.equals($scope.policy, oldCopy); + }, true); + + $scope.addPolicy = function(policy){ + policy.value = policy.defaultValue; + if (!$scope.policy) { + $scope.policy = []; + } + $scope.policy.push(policy); + } + + $scope.removePolicy = function(index){ + $scope.policy.splice(index, 1); + } + + $scope.save = function() { + $scope.realm.passwordPolicy = toString($scope.policy); + console.log($scope.realm.passwordPolicy); + + Realm.update($scope.realm, function () { + $route.reload(); + Notifications.success("Your changes have been saved to the realm."); + }); + }; + + $scope.reset = function() { + $route.reload(); + }; +}); + +module.controller('RealmDefaultRolesCtrl', function ($scope, $route, Realm, realm, roles, Notifications, ClientRole, Client) { + + console.log('RealmDefaultRolesCtrl'); + + $scope.realm = realm; + + $scope.availableRealmRoles = []; + $scope.selectedRealmRoles = []; + $scope.selectedRealmDefRoles = []; + + $scope.availableClientRoles = []; + $scope.selectedClientRoles = []; + $scope.selectedClientDefRoles = []; + + if (!$scope.realm.hasOwnProperty('defaultRoles') || $scope.realm.defaultRoles === null) { + $scope.realm.defaultRoles = []; + } + + // Populate available roles. Available roles are neither already assigned + for (var i = 0; i < roles.length; i++) { + var item = roles[i].name; + + if ($scope.realm.defaultRoles.indexOf(item) < 0) { + $scope.availableRealmRoles.push(item); + } + } + + $scope.addRealmDefaultRole = function () { + + // Remove selected roles from the Available roles and add them to realm default roles (move from left to right). + for (var i = 0; i < $scope.selectedRealmRoles.length; i++) { + var selectedRole = $scope.selectedRealmRoles[i]; + + $scope.realm.defaultRoles.push(selectedRole); + + var index = $scope.availableRealmRoles.indexOf(selectedRole); + if (index > -1) { + $scope.availableRealmRoles.splice(index, 1); + } + } + + $scope.selectedRealmRoles = []; + + // Update/save the realm with new default roles. + Realm.update($scope.realm, function () { + Notifications.success("Realm default roles updated."); + }); + }; + + $scope.deleteRealmDefaultRole = function () { + + // Remove selected roles from the realm default roles and add them to available roles (move from right to left). + for (var i = 0; i < $scope.selectedRealmDefRoles.length; i++) { + $scope.availableRealmRoles.push($scope.selectedRealmDefRoles[i]); + + var index = $scope.realm.defaultRoles.indexOf($scope.selectedRealmDefRoles[i]); + if (index > -1) { + $scope.realm.defaultRoles.splice(index, 1); + } + } + + $scope.selectedRealmDefRoles = []; + + // Update/save the realm with new default roles. + //var realmCopy = angular.copy($scope.realm); + Realm.update($scope.realm, function () { + Notifications.success("Realm default roles updated."); + }); + }; + + $scope.changeClient = function (client) { + $scope.selectedClient = client; + $scope.selectedClientRoles = []; + $scope.selectedClientDefRoles = []; + if (!client || !client.id) { + $scope.selectedClient = null; + return; + } + + // Populate available roles for selected client + if ($scope.selectedClient) { + ClientRole.query({realm: $scope.realm.realm, client: $scope.selectedClient.id}, function (appDefaultRoles) { + if (!$scope.selectedClient.hasOwnProperty('defaultRoles') || $scope.selectedClient.defaultRoles === null) { + $scope.selectedClient.defaultRoles = []; + } + + $scope.availableClientRoles = []; + console.log('default roles', appDefaultRoles); + for (var i = 0; i < appDefaultRoles.length; i++) { + + var roleName = appDefaultRoles[i].name; + if ($scope.selectedClient.defaultRoles.indexOf(roleName) < 0) { + $scope.availableClientRoles.push(roleName); + } + } + }); + } else { + $scope.availableClientRoles = null; + } + }; + + $scope.addClientDefaultRole = function () { + + // Remove selected roles from the app available roles and add them to app default roles (move from left to right). + for (var i = 0; i < $scope.selectedClientRoles.length; i++) { + var role = $scope.selectedClientRoles[i]; + + var idx = $scope.selectedClient.defaultRoles.indexOf(role); + if (idx < 0) { + $scope.selectedClient.defaultRoles.push(role); + } + + idx = $scope.availableClientRoles.indexOf(role); + + if (idx != -1) { + $scope.availableClientRoles.splice(idx, 1); + } + } + + $scope.selectedClientRoles = []; + + // Update/save the selected client with new default roles. + delete $scope.selectedClient.text; + Client.update({ + realm: $scope.realm.realm, + client: $scope.selectedClient.id + }, $scope.selectedClient, function () { + Notifications.success("Your changes have been saved to the client."); + Client.get({realm: realm.realm, client: $scope.selectedClient.id}, function(response) { + response.text = response.clientId; + $scope.changeClient(response); + }); + }); + }; + + $scope.rmClientDefaultRole = function () { + + // Remove selected roles from the app default roles and add them to app available roles (move from right to left). + for (var i = 0; i < $scope.selectedClientDefRoles.length; i++) { + var role = $scope.selectedClientDefRoles[i]; + var idx = $scope.selectedClient.defaultRoles.indexOf(role); + if (idx != -1) { + $scope.selectedClient.defaultRoles.splice(idx, 1); + } + idx = $scope.availableClientRoles.indexOf(role); + if (idx < 0) { + $scope.availableClientRoles.push(role); + } + } + + $scope.selectedClientDefRoles = []; + + // Update/save the selected client with new default roles. + delete $scope.selectedClient.text; + Client.update({ + realm: $scope.realm.realm, + client: $scope.selectedClient.id + }, $scope.selectedClient, function () { + Notifications.success("Your changes have been saved to the client."); + Client.get({realm: realm.realm, client: $scope.selectedClient.id}, function(response) { + response.text = response.clientId; + $scope.changeClient(response); + }); + }); + }; + + clientSelectControl($scope, $route.current.params.realm, Client); +}); + + + +module.controller('IdentityProviderTabCtrl', function(Dialog, $scope, Current, Notifications, $location) { + $scope.removeIdentityProvider = function() { + Dialog.confirmDelete($scope.identityProvider.alias, 'provider', function() { + $scope.identityProvider.$remove({ + realm : Current.realm.realm, + alias : $scope.identityProvider.alias + }, function() { + $location.url("/realms/" + Current.realm.realm + "/identity-provider-settings"); + Notifications.success("The identity provider has been deleted."); + }); + }); + }; +}); + +module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, $http, $route, realm, instance, providerFactory, IdentityProvider, serverInfo, authFlows, $location, Notifications, Dialog) { + $scope.realm = angular.copy(realm); + + $scope.initSamlProvider = function() { + $scope.nameIdFormats = [ + /* + { + format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", + name: "Transient" + }, + */ + { + format: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + name: "Persistent" + + }, + { + format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + name: "Email" + + }, + { + format: "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos", + name: "Kerberos" + + }, + { + format: "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName", + name: "X.509 Subject Name" + + }, + { + format: "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName", + name: "Windows Domain Qualified Name" + + }, + { + format: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + name: "Unspecified" + + } + ]; + $scope.signatureAlgorithms = [ + "RSA_SHA1", + "RSA_SHA256", + "RSA_SHA256_MGF1", + "RSA_SHA512", + "RSA_SHA512_MGF1", + "DSA_SHA1" + ]; + $scope.xmlKeyNameTranformers = [ + "NONE", + "KEY_ID", + "CERT_SUBJECT" + ]; + $scope.principalTypes = [ + { + type: "SUBJECT", + name: "Subject NameID" + + }, + { + type: "ATTRIBUTE", + name: "Attribute [Name]" + + }, + { + type: "FRIENDLY_ATTRIBUTE", + name: "Attribute [Friendly Name]" + + } + ]; + if (instance && instance.alias) { + + } else { + $scope.identityProvider.config.nameIDPolicyFormat = $scope.nameIdFormats[0].format; + $scope.identityProvider.config.principalType = $scope.principalTypes[0].type; + $scope.identityProvider.config.signatureAlgorithm = $scope.signatureAlgorithms[1]; + $scope.identityProvider.config.xmlSigKeyInfoKeyNameTransformer = $scope.xmlKeyNameTranformers[1]; + } + $scope.identityProvider.config.entityId = $scope.identityProvider.config.entityId || (authUrl + '/realms/' + realm.realm); + } + + $scope.hidePassword = true; + $scope.fromUrl = { + data: '' + }; + + if (instance && instance.alias) { + $scope.identityProvider = angular.copy(instance); + $scope.newIdentityProvider = false; + for (var i in serverInfo.identityProviders) { + var provider = serverInfo.identityProviders[i]; + + if (provider.id == instance.providerId) { + $scope.provider = provider; + } + } + } else { + $scope.identityProvider = {}; + $scope.identityProvider.config = {}; + $scope.identityProvider.alias = providerFactory.id; + $scope.identityProvider.providerId = providerFactory.id; + + $scope.identityProvider.enabled = true; + $scope.identityProvider.authenticateByDefault = false; + $scope.identityProvider.firstBrokerLoginFlowAlias = 'first broker login'; + $scope.identityProvider.config.useJwksUrl = 'true'; + $scope.identityProvider.config.syncMode = 'IMPORT'; + $scope.newIdentityProvider = true; + } + + $scope.changed = $scope.newIdentityProvider; + + $scope.$watch('identityProvider', function() { + if (!angular.equals($scope.identityProvider, instance)) { + $scope.changed = true; + } + }, true); + + + $scope.serverInfo = serverInfo; + + $scope.allProviders = angular.copy(serverInfo.identityProviders); + + $scope.configuredProviders = angular.copy(realm.identityProviders); + + removeUsedSocial(); + + $scope.authFlows = []; + for (var i=0 ; i 0) { + $scope.importUrl = true; + } else{ + $scope.importUrl = false; + } + }); + + $scope.$watch('configuredProviders', function(configuredProviders) { + if (configuredProviders) { + $scope.configuredProviders = angular.copy(configuredProviders); + + for (var j = 0; j < configuredProviders.length; j++) { + var configProvidedId = configuredProviders[j].providerId; + + for (var i in $scope.allProviders) { + var provider = $scope.allProviders[i]; + if (provider.id == configProvidedId) { + configuredProviders[j].provider = provider; + } + } + } + $scope.configuredProviders = angular.copy(configuredProviders); + } + }, true); + + $scope.callbackUrl = authServerUrl + "/realms/" + realm.realm + "/broker/"; + + $scope.addProvider = function(provider) { + $location.url("/create/identity-provider/" + realm.realm + "/" + provider.id); + }; + + $scope.save = function() { + if ($scope.newIdentityProvider) { + if (!$scope.identityProvider.alias) { + Notifications.error("You must specify an alias"); + return; + } + IdentityProvider.save({ + realm: $scope.realm.realm, alias: '' + }, $scope.identityProvider, function () { + $location.url("/realms/" + realm.realm + "/identity-provider-settings/provider/" + $scope.identityProvider.providerId + "/" + $scope.identityProvider.alias); + Notifications.success("The " + $scope.identityProvider.alias + " provider has been created."); + }); + } else { + IdentityProvider.update({ + realm: $scope.realm.realm, + alias: $scope.identityProvider.alias + }, $scope.identityProvider, function () { + $route.reload(); + Notifications.success("The " + $scope.identityProvider.alias + " provider has been updated."); + }); + } + }; + + $scope.cancel = function() { + if ($scope.newIdentityProvider) { + $location.url("/realms/" + realm.realm + "/identity-provider-settings"); + } else { + $route.reload(); + } + }; + + + $scope.reset = function() { + $scope.identityProvider = {}; + $scope.configuredProviders = angular.copy($scope.realm.identityProviders); + }; + + $scope.showPassword = function(flag) { + $scope.hidePassword = flag; + }; + + $scope.removeIdentityProvider = function(identityProvider) { + Dialog.confirmDelete(identityProvider.alias, 'provider', function() { + IdentityProvider.remove({ + realm : realm.realm, + alias : identityProvider.alias + }, function() { + $route.reload(); + Notifications.success("The identity provider has been deleted."); + }); + }); + }; + + // KEYCLOAK-5932: remove social providers that have already been defined + function removeUsedSocial() { + var i = $scope.allProviders.length; + while (i--) { + if ($scope.allProviders[i].groupName !== 'Social') continue; + if ($scope.configuredProviders != null) { + for (var j = 0; j < $scope.configuredProviders.length; j++) { + if ($scope.configuredProviders[j].providerId === $scope.allProviders[i].id) { + $scope.allProviders.splice(i, 1); + break; + } + } + } + } + }; + + if (instance && instance.alias) { + try { $scope.authnContextClassRefs = JSON.parse($scope.identityProvider.config.authnContextClassRefs || '[]'); } catch (e) { $scope.authnContextClassRefs = []; } + try { $scope.authnContextDeclRefs = JSON.parse($scope.identityProvider.config.authnContextDeclRefs || '[]'); } catch (e) { $scope.authnContextDeclRefs = []; } + } else { + $scope.authnContextClassRefs = []; + $scope.authnContextDeclRefs = []; + } + + $scope.deleteAuthnContextClassRef = function(index) { + $scope.authnContextClassRefs.splice(index, 1); + $scope.identityProvider.config.authnContextClassRefs = JSON.stringify($scope.authnContextClassRefs); + }; + + $scope.addAuthnContextClassRef = function() { + $scope.authnContextClassRefs.push($scope.newAuthnContextClassRef); + $scope.identityProvider.config.authnContextClassRefs = JSON.stringify($scope.authnContextClassRefs); + $scope.newAuthnContextClassRef = ""; + }; + + $scope.deleteAuthnContextDeclRef = function(index) { + $scope.authnContextDeclRefs.splice(index, 1); + $scope.identityProvider.config.authnContextDeclRefs = JSON.stringify($scope.authnContextDeclRefs); + }; + + $scope.addAuthnContextDeclRef = function() { + $scope.authnContextDeclRefs.push($scope.newAuthnContextDeclRef); + $scope.identityProvider.config.authnContextDeclRefs = JSON.stringify($scope.authnContextDeclRefs); + $scope.newAuthnContextDeclRef = ""; + }; +}); + +module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, TimeUnit, TimeUnit2, serverInfo) { + $scope.realm = realm; + $scope.serverInfo = serverInfo; + $scope.actionTokenProviders = $scope.serverInfo.providers.actionTokenHandler.providers; + + $scope.realm.accessTokenLifespan = TimeUnit2.asUnit(realm.accessTokenLifespan); + $scope.realm.accessTokenLifespanForImplicitFlow = TimeUnit2.asUnit(realm.accessTokenLifespanForImplicitFlow); + $scope.realm.ssoSessionIdleTimeout = TimeUnit2.asUnit(realm.ssoSessionIdleTimeout); + $scope.realm.ssoSessionMaxLifespan = TimeUnit2.asUnit(realm.ssoSessionMaxLifespan); + $scope.realm.ssoSessionIdleTimeoutRememberMe = TimeUnit2.asUnit(realm.ssoSessionIdleTimeoutRememberMe); + $scope.realm.ssoSessionMaxLifespanRememberMe = TimeUnit2.asUnit(realm.ssoSessionMaxLifespanRememberMe); + $scope.realm.offlineSessionIdleTimeout = TimeUnit2.asUnit(realm.offlineSessionIdleTimeout); + // KEYCLOAK-7688 Offline Session Max for Offline Token + $scope.realm.offlineSessionMaxLifespan = TimeUnit2.asUnit(realm.offlineSessionMaxLifespan); + $scope.realm.clientSessionIdleTimeout = TimeUnit2.asUnit(realm.clientSessionIdleTimeout); + $scope.realm.clientSessionMaxLifespan = TimeUnit2.asUnit(realm.clientSessionMaxLifespan); + $scope.realm.clientOfflineSessionIdleTimeout = TimeUnit2.asUnit(realm.clientOfflineSessionIdleTimeout); + $scope.realm.clientOfflineSessionMaxLifespan = TimeUnit2.asUnit(realm.clientOfflineSessionMaxLifespan); + $scope.realm.accessCodeLifespan = TimeUnit2.asUnit(realm.accessCodeLifespan); + $scope.realm.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin); + $scope.realm.accessCodeLifespanUserAction = TimeUnit2.asUnit(realm.accessCodeLifespanUserAction); + $scope.realm.actionTokenGeneratedByAdminLifespan = TimeUnit2.asUnit(realm.actionTokenGeneratedByAdminLifespan); + $scope.realm.actionTokenGeneratedByUserLifespan = TimeUnit2.asUnit(realm.actionTokenGeneratedByUserLifespan); + $scope.realm.attributes = realm.attributes + + var oldCopy = angular.copy($scope.realm); + $scope.changed = false; + + $scope.$watch('realm', function() { + if (!angular.equals($scope.realm, oldCopy)) { + $scope.changed = true; + } + }, true); + + $scope.$watch('actionLifespanId', function () { + // changedActionLifespanId signals other watchers that we were merely + // changing the dropdown and we should not enable 'save' button + if ($scope.actionTokenAttribute && $scope.actionTokenAttribute.hasOwnProperty('time')) { + $scope.changedActionLifespanId = true; + } + + $scope.actionTokenAttribute = TimeUnit2.asUnit($scope.realm.attributes['actionTokenGeneratedByUserLifespan.' + $scope.actionLifespanId]); + }, true); + + $scope.$watch('actionTokenAttribute', function () { + if ($scope.actionLifespanId === null) return; + + if ($scope.changedActionLifespanId) { + $scope.changedActionLifespanId = false; + return; + } else { + $scope.changed = true; + } + + if ($scope.actionTokenAttribute !== null) { + $scope.realm.attributes['actionTokenGeneratedByUserLifespan.' + $scope.actionLifespanId] = $scope.actionTokenAttribute.toSeconds(); + } + }, true); + + $scope.changeRevokeRefreshToken = function() { + + }; + + $scope.save = function() { + $scope.realm.accessTokenLifespan = $scope.realm.accessTokenLifespan.toSeconds(); + $scope.realm.accessTokenLifespanForImplicitFlow = $scope.realm.accessTokenLifespanForImplicitFlow.toSeconds(); + $scope.realm.ssoSessionIdleTimeout = $scope.realm.ssoSessionIdleTimeout.toSeconds(); + $scope.realm.ssoSessionMaxLifespan = $scope.realm.ssoSessionMaxLifespan.toSeconds(); + $scope.realm.ssoSessionIdleTimeoutRememberMe = $scope.realm.ssoSessionIdleTimeoutRememberMe.toSeconds(); + $scope.realm.ssoSessionMaxLifespanRememberMe = $scope.realm.ssoSessionMaxLifespanRememberMe.toSeconds(); + $scope.realm.offlineSessionIdleTimeout = $scope.realm.offlineSessionIdleTimeout.toSeconds(); + // KEYCLOAK-7688 Offline Session Max for Offline Token + $scope.realm.offlineSessionMaxLifespan = $scope.realm.offlineSessionMaxLifespan.toSeconds(); + $scope.realm.clientSessionIdleTimeout = $scope.realm.clientSessionIdleTimeout.toSeconds(); + $scope.realm.clientSessionMaxLifespan = $scope.realm.clientSessionMaxLifespan.toSeconds(); + $scope.realm.clientOfflineSessionIdleTimeout = $scope.realm.clientOfflineSessionIdleTimeout.toSeconds(); + $scope.realm.clientOfflineSessionMaxLifespan = $scope.realm.clientOfflineSessionMaxLifespan.toSeconds(); + $scope.realm.accessCodeLifespan = $scope.realm.accessCodeLifespan.toSeconds(); + $scope.realm.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds(); + $scope.realm.accessCodeLifespanLogin = $scope.realm.accessCodeLifespanLogin.toSeconds(); + $scope.realm.actionTokenGeneratedByAdminLifespan = $scope.realm.actionTokenGeneratedByAdminLifespan.toSeconds(); + $scope.realm.actionTokenGeneratedByUserLifespan = $scope.realm.actionTokenGeneratedByUserLifespan.toSeconds(); + + Realm.update($scope.realm, function () { + $route.reload(); + Notifications.success("The changes have been saved to the realm."); + }); + }; + + $scope.resetToDefaultToken = function (actionTokenId) { + $scope.actionTokenAttribute = {}; + delete $scope.realm.attributes['actionTokenGeneratedByUserLifespan.' + $scope.actionLifespanId]; + //Only for UI effects, resets to the original state + $scope.actionTokenAttribute.unit = 'Minutes'; + } + + $scope.reset = function() { + $route.reload(); + }; +}); + +module.controller('ViewKeyCtrl', function($scope, key) { + $scope.key = key; +}); + +module.controller('RealmKeysCtrl', function($scope, Realm, realm, $http, $route, $location, Dialog, Notifications, serverInfo, keys, Components, $modal) { + $scope.realm = angular.copy(realm); + $scope.keys = keys.keys; + $scope.active = {}; + + Components.query({realm: realm.realm, + parent: realm.id, + type: 'org.keycloak.keys.KeyProvider' + }, function(data) { + for (var i = 0; i < keys.keys.length; i++) { + for (var j = 0; j < data.length; j++) { + if (keys.keys[i].providerId == data[j].id) { + keys.keys[i].provider = data[j]; + } + } + } + + for (var t in keys.active) { + for (var i = 0; i < keys.keys.length; i++) { + if (keys.active[t] == keys.keys[i].kid) { + $scope.active[t] = keys.keys[i]; + } + } + } + }); + + $scope.viewKey = function(key) { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/view-key.html', + controller: 'ViewKeyCtrl', + resolve: { + key: function () { + return key; + } + } + }) + } +}); + +module.controller('RealmKeysProvidersCtrl', function($scope, Realm, realm, $http, $route, $location, Dialog, Notifications, serverInfo, Components, $modal) { + $scope.realm = angular.copy(realm); + $scope.enableUpload = false; + + $scope.providers = serverInfo.componentTypes['org.keycloak.keys.KeyProvider']; + + Components.query({realm: realm.realm, + parent: realm.id, + type: 'org.keycloak.keys.KeyProvider' + }, function(data) { + $scope.instances = data; + + for (var i = 0; i < $scope.instances.length; i++) { + for (var j = 0; j < $scope.providers.length; j++) { + if ($scope.providers[j].id === $scope.instances[i].providerId) { + $scope.instances[i].provider = $scope.providers[j]; + } + } + } + }); + + $scope.addProvider = function(provider) { + $location.url("/create/keys/" + realm.realm + "/providers/" + provider.id); + }; + + $scope.removeInstance = function(instance) { + Dialog.confirmDelete(instance.name, 'key provider', function() { + Components.remove({ + realm : realm.realm, + componentId : instance.id + }, function() { + $route.reload(); + Notifications.success("The provider has been deleted."); + }); + }); + }; +}); + +module.controller('GenericKeystoreCtrl', function($scope, $location, Notifications, $route, Dialog, realm, serverInfo, instance, providerId, Components) { + $scope.create = !instance.providerId; + $scope.realm = realm; + + var providers = serverInfo.componentTypes['org.keycloak.keys.KeyProvider']; + var providerFactory = null; + for (var i = 0; i < providers.length; i++) { + var p = providers[i]; + if (p.id == providerId) { + $scope.providerFactory = p; + providerFactory = p; + break; + } + } + + if ($scope.create) { + $scope.instance = { + name: providerFactory.id, + providerId: providerFactory.id, + providerType: 'org.keycloak.keys.KeyProvider', + parentId: realm.id, + config: { + 'priority': ["0"] + } + } + } else { + $scope.instance = angular.copy(instance); + } + + if (providerFactory.properties) { + for (var i = 0; i < providerFactory.properties.length; i++) { + var configProperty = providerFactory.properties[i]; + if (!$scope.instance.config[configProperty.name]) { + if (configProperty.defaultValue) { + $scope.instance.config[configProperty.name] = [configProperty.defaultValue]; + if (!$scope.create) { + instance.config[configProperty.name] = [configProperty.defaultValue]; + } + } else { + $scope.instance.config[configProperty.name] = ['']; + if (!$scope.create) { + instance.config[configProperty.name] = [configProperty.defaultValue]; + } + } + } + } + } + + $scope.$watch('instance', function() { + if (!angular.equals($scope.instance, instance)) { + $scope.changed = true; + } + + }, true); + + $scope.save = function() { + $scope.changed = false; + if ($scope.create) { + Components.save({realm: realm.realm}, $scope.instance, function (data, headers) { + var l = headers().location; + var id = l.substring(l.lastIndexOf("/") + 1); + + $location.url("/realms/" + realm.realm + "/keys/providers/" + $scope.instance.providerId + "/" + id); + Notifications.success("The provider has been created."); + }); + } else { + Components.update({realm: realm.realm, + componentId: instance.id + }, + $scope.instance, function () { + $route.reload(); + Notifications.success("The provider has been updated."); + }); + } + }; + + $scope.reset = function() { + $route.reload(); + }; + + $scope.cancel = function() { + if ($scope.create) { + $location.url("/realms/" + realm.realm + "/keys"); + } else { + $route.reload(); + } + }; +}); + +module.controller('RealmSessionStatsCtrl', function($scope, realm, stats, RealmClientSessionStats, RealmLogoutAll, Notifications) { + $scope.realm = realm; + $scope.stats = stats; + + $scope.logoutAll = function() { + RealmLogoutAll.save({realm : realm.realm}, function (globalReqResult) { + var successCount = globalReqResult.successRequests ? globalReqResult.successRequests.length : 0; + var failedCount = globalReqResult.failedRequests ? globalReqResult.failedRequests.length : 0; + + if (failedCount > 0) { + var msgStart = successCount>0 ? 'Successfully logout all users under: ' + globalReqResult.successRequests + ' . ' : ''; + Notifications.error(msgStart + 'Failed to logout users under: ' + globalReqResult.failedRequests + '. Verify availability of failed hosts and try again'); + } else { + window.location.reload(); + } + }); + }; +}); + + +module.controller('RealmRevocationCtrl', function($scope, Realm, RealmPushRevocation, realm, $http, $location, Dialog, Notifications) { + $scope.realm = angular.copy(realm); + + var setNotBefore = function() { + if ($scope.realm.notBefore == 0) { + $scope.notBefore = "None"; + } else { + $scope.notBefore = new Date($scope.realm.notBefore * 1000); + } + }; + + setNotBefore(); + + var reset = function() { + Realm.get({ id : realm.realm }, function(updated) { + $scope.realm = updated; + setNotBefore(); + }) + + }; + + $scope.clear = function() { + Realm.update({ realm: realm.realm, notBefore : 0 }, function () { + $scope.notBefore = "None"; + Notifications.success('Not Before cleared for realm.'); + reset(); + }); + } + $scope.setNotBeforeNow = function() { + Realm.update({ realm: realm.realm, notBefore : new Date().getTime()/1000}, function () { + Notifications.success('Not Before set for realm.'); + reset(); + }); + } + $scope.pushRevocation = function() { + RealmPushRevocation.save({ realm: realm.realm}, function (globalReqResult) { + var successCount = globalReqResult.successRequests ? globalReqResult.successRequests.length : 0; + var failedCount = globalReqResult.failedRequests ? globalReqResult.failedRequests.length : 0; + + if (successCount==0 && failedCount==0) { + Notifications.warn('No push sent. No admin URI configured or no registered cluster nodes available'); + } else if (failedCount > 0) { + var msgStart = successCount>0 ? 'Successfully push notBefore to: ' + globalReqResult.successRequests + ' . ' : ''; + Notifications.error(msgStart + 'Failed to push notBefore to: ' + globalReqResult.failedRequests + '. Verify availability of failed hosts and try again'); + } else { + Notifications.success('Successfully push notBefore to all configured clients'); + } + }); + } + +}); + + +module.controller('RoleTabCtrl', function(Dialog, $scope, Current, Notifications, $location) { + $scope.removeRole = function() { + Dialog.confirmDelete($scope.role.name, 'role', function() { + RoleById.remove({ + realm: realm.realm, + role: $scope.role.id + }, function () { + $route.reload(); + Notifications.success("The role has been deleted."); + }); + }); + }; +}); + + +module.controller('RoleListCtrl', function($scope, $route, Dialog, Notifications, realm, RoleList, RoleById, filterFilter) { + $scope.realm = realm; + $scope.roles = []; + + $scope.query = { + realm: realm.realm, + search : null, + max : 20, + first : 0 + } + + $scope.$watch('query.search', function (newVal, oldVal) { + if($scope.query.search && $scope.query.search.length >= 3) { + $scope.firstPage(); + } + }, true); + + $scope.firstPage = function() { + $scope.query.first = 0; + $scope.searchQuery(); + } + + $scope.previousPage = function() { + $scope.query.first -= parseInt($scope.query.max); + if ($scope.query.first < 0) { + $scope.query.first = 0; + } + $scope.searchQuery(); + } + + $scope.nextPage = function() { + $scope.query.first += parseInt($scope.query.max); + $scope.searchQuery(); + } + + $scope.searchQuery = function() { + $scope.searchLoaded = false; + + $scope.roles = RoleList.query($scope.query, function() { + $scope.searchLoaded = true; + $scope.lastSearch = $scope.query.search; + }); + }; + + $scope.searchQuery(); + + $scope.removeRole = function (role) { + Dialog.confirmDelete(role.name, 'role', function () { + RoleById.remove({ + realm: realm.realm, + role: role.id + }, function () { + $route.reload(); + Notifications.success("The role has been deleted."); + }); + }); + }; +}); + + +module.controller('RoleDetailCtrl', function($scope, realm, role, roles, Client, $route, + Role, ClientRole, RoleById, RoleRealmComposites, RoleClientComposites, + $http, $location, Dialog, Notifications, RealmRoleRemover, ComponentUtils) { + $scope.realm = realm; + $scope.role = angular.copy(role); + $scope.create = !role.name; + + $scope.changed = $scope.create; + + $scope.save = function() { + convertAttributeValuesToLists(); + console.log('save'); + if ($scope.create) { + Role.save({ + realm: realm.realm + }, $scope.role, function (data, headers) { + $scope.changed = false; + convertAttributeValuesToString($scope.role); + role = angular.copy($scope.role); + + Role.get({ realm: realm.realm, role: role.name }, function(role) { + var id = role.id; + $location.url("/realms/" + realm.realm + "/roles/" + id); + Notifications.success("The role has been created."); + }); + }); + } else { + $scope.update(); + } + }; + + $scope.remove = function() { + RealmRoleRemover.remove($scope.role, realm, Dialog, $location, Notifications); + }; + + $scope.cancel = function () { + $location.url("/realms/" + realm.realm + "/roles"); + }; + + $scope.addAttribute = function() { + $scope.role.attributes[$scope.newAttribute.key] = $scope.newAttribute.value; + delete $scope.newAttribute; + } + + $scope.removeAttribute = function(key) { + delete $scope.role.attributes[key]; + } + + function convertAttributeValuesToLists() { + var attrs = $scope.role.attributes; + for (var attribute in attrs) { + if (typeof attrs[attribute] === "string") { + var attrVals = attrs[attribute].split("##"); + attrs[attribute] = attrVals; + } + } + } + + function convertAttributeValuesToString(role) { + var attrs = role.attributes; + for (var attribute in attrs) { + if (typeof attrs[attribute] === "object") { + var attrVals = attrs[attribute].join("##"); + attrs[attribute] = attrVals; + console.log("attribute" + attrVals) + } + } + } + + roleControl($scope, $route, realm, role, roles, Client, + ClientRole, RoleById, RoleRealmComposites, RoleClientComposites, + $http, $location, Notifications, Dialog, ComponentUtils); +}); + +module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications, RealmSMTPConnectionTester) { + console.log('RealmSMTPSettingsCtrl'); + + var booleanSmtpAtts = ["auth","ssl","starttls"]; + + $scope.realm = realm; + + if ($scope.realm.smtpServer) { + $scope.realm.smtpServer = typeObject($scope.realm.smtpServer); + }; + + var oldCopy = angular.copy($scope.realm); + $scope.changed = false; + + $scope.$watch('realm', function() { + if (!angular.equals($scope.realm, oldCopy)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + var realmCopy = angular.copy($scope.realm); + realmCopy['smtpServer'] = detypeObject(realmCopy.smtpServer); + $scope.changed = false; + Realm.update(realmCopy, function () { + $location.url("/realms/" + realm.realm + "/smtp-settings"); + Notifications.success("Your changes have been saved to the realm."); + }); + }; + + $scope.reset = function() { + $scope.realm = angular.copy(oldCopy); + $scope.changed = false; + }; + + $scope.testConnection = function() { + RealmSMTPConnectionTester.save({realm: realm.realm}, realm.smtpServer, function() { + Notifications.success("SMTP connection successful. E-mail was sent!"); + }, function(errorResponse) { + if (error.data.errorMessage) { + Notifications.error(error.data.errorMessage); + } else { + Notifications.error('Unexpected error during SMTP validation'); + } + }); + }; + + /* Convert string attributes containing a boolean to actual boolean type + convert an integer string (port) to integer. */ + function typeObject(obj){ + for (var att in obj){ + if (booleanSmtpAtts.indexOf(att) < 0) + continue; + if (obj[att] === "true"){ + obj[att] = true; + } else if (obj[att] === "false"){ + obj[att] = false; + } + } + + obj['port'] = parseInt(obj['port']); + + return obj; + } + + /* Convert all non-string values to strings to invert changes caused by the typeObject function. */ + function detypeObject(obj){ + for (var att in obj){ + if (booleanSmtpAtts.indexOf(att) < 0) + continue; + if (obj[att] === true){ + obj[att] = "true"; + } else if (obj[att] === false){ + obj[att] = "false" + } + } + + obj['port'] = obj['port'] && obj['port'].toString(); + + return obj; + } +}); + +module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmEventsConfig, RealmEvents, RealmAdminEvents, realm, serverInfo, $location, Notifications, TimeUnit, Dialog) { + $scope.realm = realm; + + $scope.eventsConfig = eventsConfig; + + $scope.eventsConfig.expirationUnit = TimeUnit.autoUnit(eventsConfig.eventsExpiration); + $scope.eventsConfig.eventsExpiration = TimeUnit.toUnit(eventsConfig.eventsExpiration, $scope.eventsConfig.expirationUnit); + + $scope.eventListeners = Object.keys(serverInfo.providers.eventsListener.providers); + + $scope.eventsConfigSelectOptions = { + 'multiple': true, + 'simple_tags': true, + 'tags': $scope.eventListeners + }; + + $scope.eventSelectOptions = { + 'multiple': true, + 'simple_tags': true, + 'tags': serverInfo.enums['eventType'] + }; + + var oldCopy = angular.copy($scope.eventsConfig); + $scope.changed = false; + + $scope.$watch('eventsConfig', function() { + if (!angular.equals($scope.eventsConfig, oldCopy)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + $scope.changed = false; + + var copy = angular.copy($scope.eventsConfig) + delete copy['expirationUnit']; + + copy.eventsExpiration = TimeUnit.toSeconds($scope.eventsConfig.eventsExpiration, $scope.eventsConfig.expirationUnit); + + RealmEventsConfig.update({ + id : realm.realm + }, copy, function () { + $location.url("/realms/" + realm.realm + "/events-settings"); + Notifications.success("Your changes have been saved to the realm."); + }); + }; + + $scope.reset = function() { + $scope.eventsConfig = angular.copy(oldCopy); + $scope.changed = false; + }; + + $scope.clearEvents = function() { + Dialog.confirmDelete($scope.realm.realm, 'events', function() { + RealmEvents.remove({ id : $scope.realm.realm }, function() { + Notifications.success("The events has been cleared."); + }); + }); + }; + + $scope.clearAdminEvents = function() { + Dialog.confirmDelete($scope.realm.realm, 'admin-events', function() { + RealmAdminEvents.remove({ id : $scope.realm.realm }, function() { + Notifications.success("The admin events has been cleared."); + }); + }); + }; +}); + +module.controller('RealmEventsCtrl', function($scope, RealmEvents, realm, serverInfo) { + $scope.realm = realm; + $scope.page = 0; + + $scope.eventSelectOptions = { + 'multiple': true, + 'simple_tags': true, + 'tags': serverInfo.enums['eventType'] + }; + + $scope.query = { + id : realm.realm, + max : 5, + first : 0 + } + + $scope.disablePaste = function(e) { + e.preventDefault(); + return false; + } + + $scope.update = function() { + $scope.query.first = 0; + for (var i in $scope.query) { + if ($scope.query[i] === '') { + delete $scope.query[i]; + } + } + $scope.events = RealmEvents.query($scope.query); + } + + $scope.reset = function() { + $scope.query.first = 0; + $scope.query.max = 5; + $scope.query.type = ''; + $scope.query.client = ''; + $scope.query.user = ''; + $scope.query.dateFrom = ''; + $scope.query.dateTo = ''; + + $scope.update(); + } + + $scope.queryUpdate = function() { + for (var i in $scope.query) { + if ($scope.query[i] === '') { + delete $scope.query[i]; + } + } + $scope.events = RealmEvents.query($scope.query); + } + + $scope.firstPage = function() { + $scope.query.first = 0; + $scope.queryUpdate(); + } + + $scope.previousPage = function() { + $scope.query.first -= parseInt($scope.query.max); + if ($scope.query.first < 0) { + $scope.query.first = 0; + } + $scope.queryUpdate(); + } + + $scope.nextPage = function() { + $scope.query.first += parseInt($scope.query.max); + $scope.queryUpdate(); + } + + $scope.update(); +}); + +module.controller('RealmAdminEventsCtrl', function($scope, RealmAdminEvents, realm, serverInfo, $modal, $filter) { + $scope.realm = realm; + $scope.page = 0; + + $scope.query = { + id : realm.realm, + max : 5, + first : 0 + }; + + $scope.adminEnabledEventOperationsOptions = { + 'multiple': true, + 'simple_tags': true, + 'tags': serverInfo.enums['operationType'] + }; + + $scope.adminEnabledEventResourceTypesOptions = { + 'multiple': true, + 'simple_tags': true, + 'tags': serverInfo.enums['resourceType'] + }; + + $scope.disablePaste = function(e) { + e.preventDefault(); + return false; + } + + $scope.update = function() { + $scope.query.first = 0; + for (var i in $scope.query) { + if ($scope.query[i] === '') { + delete $scope.query[i]; + } + } + $scope.events = RealmAdminEvents.query($scope.query); + }; + + $scope.reset = function() { + $scope.query.first = 0; + $scope.query.max = 5; + $scope.query.operationTypes = ''; + $scope.query.resourceTypes = ''; + $scope.query.resourcePath = ''; + $scope.query.authRealm = ''; + $scope.query.authClient = ''; + $scope.query.authUser = ''; + $scope.query.authIpAddress = ''; + $scope.query.dateFrom = ''; + $scope.query.dateTo = ''; + + $scope.update(); + }; + + $scope.queryUpdate = function() { + for (var i in $scope.query) { + if ($scope.query[i] === '') { + delete $scope.query[i]; + } + } + $scope.events = RealmAdminEvents.query($scope.query); + } + + $scope.firstPage = function() { + $scope.query.first = 0; + $scope.queryUpdate(); + } + + $scope.previousPage = function() { + $scope.query.first -= parseInt($scope.query.max); + if ($scope.query.first < 0) { + $scope.query.first = 0; + } + $scope.queryUpdate(); + } + + $scope.nextPage = function() { + $scope.query.first += parseInt($scope.query.max); + $scope.queryUpdate(); + } + + $scope.update(); + + $scope.viewRepresentation = function(event) { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/realm-events-admin-representation.html', + controller: 'RealmAdminEventsModalCtrl', + resolve: { + event: function () { + return event; + } + } + }) + } + + $scope.viewAuth = function(event) { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/realm-events-admin-auth.html', + controller: 'RealmAdminEventsModalCtrl', + resolve: { + event: function () { + return event; + } + } + }) + } +}); + +module.controller('RealmAdminEventsModalCtrl', function($scope, $filter, event) { + $scope.event = event; +}); + +module.controller('RealmBruteForceCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications, TimeUnit, $route) { + console.log('RealmBruteForceCtrl'); + + $scope.realm = realm; + + $scope.realm.waitIncrementUnit = TimeUnit.autoUnit(realm.waitIncrementSeconds); + $scope.realm.waitIncrement = TimeUnit.toUnit(realm.waitIncrementSeconds, $scope.realm.waitIncrementUnit); + + $scope.realm.minimumQuickLoginWaitUnit = TimeUnit.autoUnit(realm.minimumQuickLoginWaitSeconds); + $scope.realm.minimumQuickLoginWait = TimeUnit.toUnit(realm.minimumQuickLoginWaitSeconds, $scope.realm.minimumQuickLoginWaitUnit); + + $scope.realm.maxFailureWaitUnit = TimeUnit.autoUnit(realm.maxFailureWaitSeconds); + $scope.realm.maxFailureWait = TimeUnit.toUnit(realm.maxFailureWaitSeconds, $scope.realm.maxFailureWaitUnit); + + $scope.realm.maxDeltaTimeUnit = TimeUnit.autoUnit(realm.maxDeltaTimeSeconds); + $scope.realm.maxDeltaTime = TimeUnit.toUnit(realm.maxDeltaTimeSeconds, $scope.realm.maxDeltaTimeUnit); + + var oldCopy = angular.copy($scope.realm); + $scope.changed = false; + + $scope.$watch('realm', function() { + if (!angular.equals($scope.realm, oldCopy)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + var realmCopy = angular.copy($scope.realm); + delete realmCopy["waitIncrementUnit"]; + delete realmCopy["waitIncrement"]; + delete realmCopy["minimumQuickLoginWaitUnit"]; + delete realmCopy["minimumQuickLoginWait"]; + delete realmCopy["maxFailureWaitUnit"]; + delete realmCopy["maxFailureWait"]; + delete realmCopy["maxDeltaTimeUnit"]; + delete realmCopy["maxDeltaTime"]; + + realmCopy.waitIncrementSeconds = TimeUnit.toSeconds($scope.realm.waitIncrement, $scope.realm.waitIncrementUnit) + realmCopy.minimumQuickLoginWaitSeconds = TimeUnit.toSeconds($scope.realm.minimumQuickLoginWait, $scope.realm.minimumQuickLoginWaitUnit) + realmCopy.maxFailureWaitSeconds = TimeUnit.toSeconds($scope.realm.maxFailureWait, $scope.realm.maxFailureWaitUnit) + realmCopy.maxDeltaTimeSeconds = TimeUnit.toSeconds($scope.realm.maxDeltaTime, $scope.realm.maxDeltaTimeUnit) + + $scope.changed = false; + Realm.update(realmCopy, function () { + oldCopy = angular.copy($scope.realm); + $location.url("/realms/" + realm.realm + "/defense/brute-force"); + Notifications.success("Your changes have been saved to the realm."); + }); + }; + + $scope.reset = function() { + $route.reload(); + }; +}); + + +module.controller('IdentityProviderMapperListCtrl', function($scope, realm, identityProvider, mapperTypes, mappers) { + $scope.realm = realm; + $scope.identityProvider = identityProvider; + $scope.mapperTypes = mapperTypes; + $scope.mappers = mappers; +}); + +module.controller('IdentityProviderMapperCtrl', function($scope, realm, identityProvider, mapperTypes, mapper, IdentityProviderMapper, Notifications, Dialog, $location) { + $scope.realm = realm; + $scope.identityProvider = identityProvider; + $scope.create = false; + $scope.mapper = angular.copy(mapper); + $scope.changed = false; + $scope.mapperType = mapperTypes[mapper.identityProviderMapper]; + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.path = $location.path().substring(1).split("/"); + }); + + $scope.$watch('mapper', function() { + if (!angular.equals($scope.mapper, mapper)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + IdentityProviderMapper.update({ + realm : realm.realm, + alias: identityProvider.alias, + mapperId : mapper.id + }, $scope.mapper, function() { + $scope.changed = false; + mapper = angular.copy($scope.mapper); + $location.url("/realms/" + realm.realm + '/identity-provider-mappers/' + identityProvider.alias + "/mappers/" + mapper.id); + Notifications.success("Your changes have been saved."); + }); + }; + + $scope.reset = function() { + $scope.mapper = angular.copy(mapper); + $scope.changed = false; + }; + + $scope.cancel = function() { + //$location.url("/realms"); + window.history.back(); + }; + + $scope.remove = function() { + Dialog.confirmDelete($scope.mapper.name, 'mapper', function() { + IdentityProviderMapper.remove({ realm: realm.realm, alias: mapper.identityProviderAlias, mapperId : $scope.mapper.id }, function() { + Notifications.success("The mapper has been deleted."); + $location.url("/realms/" + realm.realm + '/identity-provider-mappers/' + identityProvider.alias + "/mappers"); + }); + }); + }; + +}); + +module.controller('IdentityProviderMapperCreateCtrl', function($scope, realm, identityProvider, mapperTypes, IdentityProviderMapper, Notifications, Dialog, $location) { + $scope.realm = realm; + $scope.identityProvider = identityProvider; + $scope.create = true; + $scope.mapper = { identityProviderAlias: identityProvider.alias, config: {}}; + $scope.mapperTypes = mapperTypes; + + // make first type the default + $scope.mapperType = mapperTypes[Object.keys(mapperTypes)[0]]; + $scope.mapper.config.syncMode = 'INHERIT'; + + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.path = $location.path().substring(1).split("/"); + }); + + $scope.save = function() { + $scope.mapper.identityProviderMapper = $scope.mapperType.id; + IdentityProviderMapper.save({ + realm : realm.realm, alias: identityProvider.alias + }, $scope.mapper, function(data, headers) { + var l = headers().location; + var id = l.substring(l.lastIndexOf("/") + 1); + $location.url("/realms/" + realm.realm + '/identity-provider-mappers/' + identityProvider.alias + "/mappers/" + id); + Notifications.success("Mapper has been created."); + }); + }; + + $scope.cancel = function() { + //$location.url("/realms"); + window.history.back(); + }; + + +}); + +module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) { + $scope.flows = []; + $scope.clientFlows = []; + for (var i=0 ; i 0) { + $scope.provider = formProviders[0]; + } + + $scope.save = function() { + $scope.flow.provider = $scope.provider.id; + CreateExecutionFlow.save({realm: realm.realm, alias: parentFlow.alias}, $scope.flow, function() { + $location.url("/realms/" + realm.realm + "/authentication/flows"); + Notifications.success("Flow Created."); + }) + } + $scope.cancel = function() { + $location.url("/realms/" + realm.realm + "/authentication/flows"); + }; +}); + +module.controller('CreateExecutionCtrl', function($scope, realm, parentFlow, formActionProviders, authenticatorProviders, clientAuthenticatorProviders, + CreateExecution, + Notifications, $location) { + $scope.realm = realm; + $scope.parentFlow = parentFlow; + + if (parentFlow.providerId == 'form-flow') { + $scope.providers = formActionProviders; + } else if (parentFlow.providerId == 'client-flow') { + $scope.providers = clientAuthenticatorProviders; + } else { + $scope.providers = authenticatorProviders; + } + + $scope.provider = {}; + if ($scope.providers.length > 0) { + $scope.provider = $scope.providers[0]; + } + + $scope.save = function() { + var execution = { + provider: $scope.provider.id + } + CreateExecution.save({realm: realm.realm, alias: parentFlow.alias}, execution, function() { + $location.url("/realms/" + realm.realm + "/authentication/flows"); + Notifications.success("Execution Created."); + }) + } + $scope.cancel = function() { + $location.url("/realms/" + realm.realm + "/authentication/flows"); + }; +}); + + + +module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flows, selectedFlow, LastFlowSelected, Dialog, + AuthenticationFlows, AuthenticationFlowsCopy, AuthenticationFlowsUpdate, AuthenticationFlowExecutions, + AuthenticationExecution, AuthenticationExecutionRaisePriority, AuthenticationExecutionLowerPriority, + $modal, Notifications, CopyDialog, UpdateDialog, $location) { + $scope.realm = realm; + $scope.flows = flows; + + if (selectedFlow !== null) { + LastFlowSelected.alias = selectedFlow; + } + + if (selectedFlow === null && LastFlowSelected.alias !== null) { + selectedFlow = LastFlowSelected.alias; + } + + if (flows.length > 0) { + $scope.flow = flows[0]; + if (selectedFlow) { + for (var i = 0; i < flows.length; i++) { + if (flows[i].alias == selectedFlow) { + $scope.flow = flows[i]; + break; + } + } + } + } + + $scope.selectFlow = function(flow) { + $location.url("/realms/" + realm.realm + '/authentication/flows/' + flow.alias); + }; + + var setupForm = function() { + AuthenticationFlowExecutions.query({realm: realm.realm, alias: $scope.flow.alias}, function(data) { + $scope.executions = data; + $scope.choicesmax = 0; + $scope.levelmax = 0; + for (var i = 0; i < $scope.executions.length; i++ ) { + var execution = $scope.executions[i]; + if (execution.requirementChoices.length > $scope.choicesmax) { + $scope.choicesmax = execution.requirementChoices.length; + } + if (execution.level > $scope.levelmax) { + $scope.levelmax = execution.level; + } + } + $scope.levelmaxempties = []; + for (j = 0; j < $scope.levelmax; j++) { + $scope.levelmaxempties.push(j); + + } + for (var i = 0; i < $scope.executions.length; i++ ) { + var execution = $scope.executions[i]; + execution.empties = []; + for (j = 0; j < $scope.choicesmax - execution.requirementChoices.length; j++) { + execution.empties.push(j); + } + execution.preLevels = []; + for (j = 0; j < execution.level; j++) { + execution.preLevels.push(j); + } + execution.postLevels = []; + for (j = execution.level; j < $scope.levelmax; j++) { + execution.postLevels.push(j); + } + } + }) + }; + + $scope.copyFlow = function() { + CopyDialog.open('Copy Authentication Flow', $scope.flow.alias, function(name) { + AuthenticationFlowsCopy.save({realm: realm.realm, alias: $scope.flow.alias}, { + newName: name + }, function() { + $location.url("/realms/" + realm.realm + '/authentication/flows/' + name); + Notifications.success("Flow copied."); + }) + }) + }; + + $scope.deleteFlow = function() { + Dialog.confirmDelete($scope.flow.alias, 'flow', function() { + $scope.removeFlow(); + }); + }; + + $scope.removeFlow = function() { + console.log('Remove flow:' + $scope.flow.alias); + if (realm.browserFlow == $scope.flow.alias) { + Notifications.error("Cannot remove flow, it is currently being used as the browser flow."); + + } else if (realm.registrationFlow == $scope.flow.alias) { + Notifications.error("Cannot remove flow, it is currently being used as the registration flow."); + + } else if (realm.directGrantFlow == $scope.flow.alias) { + Notifications.error("Cannot remove flow, it is currently being used as the direct grant flow."); + + } else if (realm.resetCredentialsFlow == $scope.flow.alias) { + Notifications.error("Cannot remove flow, it is currently being used as the reset credentials flow."); + + } else if (realm.clientAuthenticationFlow == $scope.flow.alias) { + Notifications.error("Cannot remove flow, it is currently being used as the client authentication flow."); + + } else if (realm.dockerAuthenticationFlow == $scope.flow.alias) { + Notifications.error("Cannot remove flow, it is currently being used as the docker authentication flow."); + + } else { + AuthenticationFlows.remove({realm: realm.realm, flow: $scope.flow.id}, function () { + $location.url("/realms/" + realm.realm + '/authentication/flows/' + flows[0].alias); + Notifications.success("Flow removed"); + }) + } + + }; + + $scope.editFlow = function(flow) { + var copy = angular.copy(flow); + UpdateDialog.open('Update Authentication Flow', copy.alias, copy.description, function(name, desc) { + copy.alias = name; + copy.description = desc; + AuthenticationFlowsUpdate.update({realm: realm.realm, flow: flow.id}, copy, function() { + $location.url("/realms/" + realm.realm + '/authentication/flows/' + name); + Notifications.success("Flow updated"); + }); + }) + }; + + $scope.addFlow = function() { + $location.url("/realms/" + realm.realm + '/authentication/flows/' + $scope.flow.id + '/create/flow/execution/' + $scope.flow.id); + + } + + $scope.addSubFlow = function(execution) { + $location.url("/realms/" + realm.realm + '/authentication/flows/' + execution.flowId + '/create/flow/execution/' + $scope.flow.alias); + + } + + $scope.addSubFlowExecution = function(execution) { + $location.url("/realms/" + realm.realm + '/authentication/flows/' + execution.flowId + '/create/execution/' + $scope.flow.alias); + + } + + $scope.addExecution = function() { + $location.url("/realms/" + realm.realm + '/authentication/flows/' + $scope.flow.id + '/create/execution/' + $scope.flow.id); + + } + + $scope.createFlow = function() { + $location.url("/realms/" + realm.realm + '/authentication/create/flow'); + } + + $scope.updateExecution = function(execution) { + var copy = angular.copy(execution); + delete copy.empties; + delete copy.levels; + delete copy.preLevels; + delete copy.postLevels; + AuthenticationFlowExecutions.update({realm: realm.realm, alias: $scope.flow.alias}, copy, function() { + Notifications.success("Auth requirement updated"); + setupForm(); + }); + + }; + + $scope.editExecutionFlow = function(execution) { + var copy = angular.copy(execution); + delete copy.empties; + delete copy.levels; + delete copy.preLevels; + delete copy.postLevels; + UpdateDialog.open('Update Execution Flow', copy.displayName, copy.description, function(name, desc) { + copy.displayName = name; + copy.description = desc; + AuthenticationFlowExecutions.update({realm: realm.realm, alias: $scope.flow.alias}, copy, function() { + Notifications.success("Execution Flow updated"); + setupForm(); + }); + }) + }; + + $scope.removeExecution = function(execution) { + console.log('removeExecution: ' + execution.id); + var exeOrFlow = execution.authenticationFlow ? 'flow' : 'execution'; + Dialog.confirmDelete(execution.displayName, exeOrFlow, function() { + AuthenticationExecution.remove({realm: realm.realm, execution: execution.id}, function() { + Notifications.success("The " + exeOrFlow + " was removed."); + setupForm(); + }); + }); + + } + + $scope.raisePriority = function(execution) { + AuthenticationExecutionRaisePriority.save({realm: realm.realm, execution: execution.id}, function() { + Notifications.success("Priority raised"); + setupForm(); + }) + } + + $scope.lowerPriority = function(execution) { + AuthenticationExecutionLowerPriority.save({realm: realm.realm, execution: execution.id}, function() { + Notifications.success("Priority lowered"); + setupForm(); + }) + } + + $scope.setupForm = setupForm; + + if (selectedFlow == null) { + $scope.selectFlow(flows[0]); + } else { + setupForm(); + } +}); + +module.controller('RequiredActionsCtrl', function($scope, realm, unregisteredRequiredActions, + $modal, $route, + RegisterRequiredAction, RequiredActions, RequiredActionRaisePriority, RequiredActionLowerPriority, Notifications) { + console.log('RequiredActionsCtrl'); + $scope.realm = realm; + $scope.unregisteredRequiredActions = unregisteredRequiredActions; + $scope.requiredActions = []; + var setupRequiredActionsForm = function() { + console.log('setupRequiredActionsForm'); + RequiredActions.query({realm: realm.realm}, function(data) { + $scope.requiredActions = []; + for (var i = 0; i < data.length; i++) { + $scope.requiredActions.push(data[i]); + } + }); + }; + + $scope.updateRequiredAction = function(action) { + RequiredActions.update({realm: realm.realm, alias: action.alias}, action, function() { + Notifications.success("Required action updated"); + setupRequiredActionsForm(); + }); + } + + $scope.raisePriority = function(action) { + RequiredActionRaisePriority.save({realm: realm.realm, alias: action.alias}, function() { + Notifications.success("Required action's priority raised"); + setupRequiredActionsForm(); + }) + } + + $scope.lowerPriority = function(action) { + RequiredActionLowerPriority.save({realm: realm.realm, alias: action.alias}, function() { + Notifications.success("Required action's priority lowered"); + setupRequiredActionsForm(); + }) + } + + $scope.register = function() { + var controller = function($scope, $modalInstance) { + $scope.unregisteredRequiredActions = unregisteredRequiredActions; + $scope.selected = { + selected: $scope.unregisteredRequiredActions[0] + } + $scope.ok = function () { + $modalInstance.close(); + RegisterRequiredAction.save({realm: realm.realm}, $scope.selected.selected, function() { + $route.reload(); + }); + }; + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + } + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/unregistered-required-action-selector.html', + controller: controller, + resolve: { + } + }); + } + + setupRequiredActionsForm(); + + +}); + +module.controller('AuthenticationConfigCtrl', function($scope, realm, flow, configType, config, AuthenticationConfig, Notifications, + Dialog, $location, ComponentUtils) { + $scope.realm = realm; + $scope.flow = flow; + $scope.configType = configType; + $scope.create = false; + $scope.config = angular.copy(config); + $scope.changed = false; + + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.path = $location.path().substring(1).split("/"); + }); + + $scope.$watch('config', function() { + if (!angular.equals($scope.config, config)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + var configCopy = angular.copy($scope.config); + ComponentUtils.convertAllListValuesToMultivaluedString(configType.properties, configCopy.config); + + AuthenticationConfig.update({ + realm : realm.realm, + config : config.id + }, configCopy, function() { + $scope.changed = false; + config = angular.copy($scope.config); + $location.url("/realms/" + realm.realm + '/authentication/flows/' + flow.id + '/config/' + configType.providerId + "/" + config.id); + Notifications.success("Your changes have been saved."); + }); + }; + + $scope.reset = function() { + $scope.config = angular.copy(config); + $scope.changed = false; + }; + + $scope.cancel = function() { + //$location.url("/realms"); + window.history.back(); + }; + + $scope.remove = function() { + Dialog.confirmDelete($scope.config.alias, 'config', function() { + AuthenticationConfig.remove({ realm: realm.realm, config : $scope.config.id }, function() { + Notifications.success("The config has been deleted."); + $location.url("/realms/" + realm.realm + '/authentication/flows/' + flow.id); + }); + }); + }; + +}); + +module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow, configType, execution, AuthenticationExecutionConfig, + Notifications, Dialog, $location, ComponentUtils) { + $scope.realm = realm; + $scope.flow = flow; + $scope.create = true; + $scope.configType = configType; + + var defaultConfig = {}; + if (configType && Array.isArray(configType.properties)) { + for(var i = 0; i < configType.properties.length; i++) { + var property = configType.properties[i]; + if (property && property.name) { + defaultConfig[property.name] = property.defaultValue; + } + } + } + + $scope.config = { config: defaultConfig}; + + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.path = $location.path().substring(1).split("/"); + }); + + $scope.save = function() { + var configCopy = angular.copy($scope.config); + ComponentUtils.convertAllListValuesToMultivaluedString(configType.properties, configCopy.config); + + AuthenticationExecutionConfig.save({ + realm : realm.realm, + execution: execution + }, configCopy, function(data, headers) { + var l = headers().location; + var id = l.substring(l.lastIndexOf("/") + 1); + var url = "/realms/" + realm.realm + '/authentication/flows/' + flow.id + '/config/' + configType.providerId + "/" + id; + console.log('redirect url: ' + url); + $location.url(url); + Notifications.success("Config has been created."); + }); + }; + + $scope.cancel = function() { + //$location.url("/realms"); + window.history.back(); + }; +}); + +module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, ClientInitialAccess, Dialog, Notifications, $route, $location) { + $scope.realm = realm; + $scope.clientInitialAccess = clientInitialAccess; + + $scope.remove = function(id) { + Dialog.confirmDelete(id, 'initial access token', function() { + ClientInitialAccess.remove({ realm: realm.realm, id: id }, function() { + Notifications.success("The initial access token was deleted."); + $route.reload(); + }); + }); + } +}); + +module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, ClientInitialAccess, TimeUnit, Dialog, $location, $translate) { + $scope.expirationUnit = 'Days'; + $scope.expiration = TimeUnit.toUnit(0, $scope.expirationUnit); + $scope.count = 1; + $scope.realm = realm; + + $scope.save = function() { + var expiration = TimeUnit.toSeconds($scope.expiration, $scope.expirationUnit); + ClientInitialAccess.save({ + realm: realm.realm + }, { expiration: expiration, count: $scope.count}, function (data) { + console.debug(data); + $scope.id = data.id; + $scope.token = data.token; + }); + }; + + $scope.cancel = function() { + $location.url('/realms/' + realm.realm + '/client-registration/client-initial-access'); + }; + + $scope.done = function() { + var btns = { + ok: { + label: $translate.instant('continue'), + cssClass: 'btn btn-primary' + }, + cancel: { + label: $translate.instant('cancel'), + cssClass: 'btn btn-default' + } + } + + var title = $translate.instant('initial-access-token.confirm.title'); + var message = $translate.instant('initial-access-token.confirm.text'); + Dialog.open(title, message, btns, function() { + $location.url('/realms/' + realm.realm + '/client-registration/client-initial-access'); + }); + }; +}); + +module.controller('ClientRegPoliciesCtrl', function($scope, realm, clientRegistrationPolicyProviders, policies, Dialog, Notifications, Components, $route, $location) { + $scope.realm = realm; + $scope.providers = clientRegistrationPolicyProviders; + $scope.anonPolicies = []; + $scope.authPolicies = []; + for (var i=0 ; i { + $scope.headerTitle = translatedValue; + }).catch(() => { + $scope.headerTitle = $scope.instance.providerId; + }); + + if ($scope.create) { + $scope.instance.name = ""; + $scope.instance.parentId = realm.id; + $scope.instance.config = {}; + + if ($scope.providerType.properties) { + + for (let i = 0; i < $scope.providerType.properties.length; i++) { + let configProperty = $scope.providerType.properties[i]; + $scope.instance.config[configProperty.name] = toDefaultValue(configProperty); + } + } + } + + if ($scope.providerType.properties) { + ComponentUtils.addLastEmptyValueToMultivaluedLists($scope.providerType.properties, $scope.instance.config); + ComponentUtils.addMvOptionsToMultivaluedLists($scope.providerType.properties); + } + + let oldCopy = angular.copy($scope.instance); + $scope.changed = false; + + $scope.$watch('instance', function() { + if (!angular.equals($scope.instance, oldCopy)) { + $scope.changed = true; + } + }, true); + + $scope.reset = function() { + $scope.create ? window.history.back() : $route.reload(); + }; + + $scope.hasValidValues = () => $scope.changed && $scope.instance.name; + + $scope.save = function() { + $scope.changed = false; + if ($scope.create) { + Components.save({realm: realm.realm}, $scope.instance, function (data, headers) { + var l = headers().location; + var id = l.substring(l.lastIndexOf("/") + 1); + $location.url("/realms/" + realm.realm + "/client-registration/client-reg-policies/" + $scope.instance.providerId + "/" + id); + Notifications.success("The policy has been created."); + }); + } else { + Components.update({realm: realm.realm, + componentId: instance.id + }, + $scope.instance, function () { + $route.reload(); + Notifications.success("The policy has been updated."); + }); + } + }; + +}); + +module.controller('RealmImportCtrl', function($scope, realm, $route, + Notifications, $modal, $resource) { + $scope.rawContent = {}; + $scope.fileContent = { + enabled: true + }; + $scope.changed = false; + $scope.files = []; + $scope.realm = realm; + $scope.overwrite = false; + $scope.skip = false; + $scope.importUsers = false; + $scope.importGroups = false; + $scope.importClients = false; + $scope.importIdentityProviders = false; + $scope.importRealmRoles = false; + $scope.importClientRoles = false; + $scope.ifResourceExists='FAIL'; + $scope.isMultiRealm = false; + $scope.results = {}; + $scope.currentPage = 0; + var pageSize = 15; + + var oldCopy = angular.copy($scope.fileContent); + + $scope.importFile = function($fileContent){ + var parsed; + try { + parsed = JSON.parse($fileContent); + } catch (e) { + Notifications.error('Unable to parse JSON file.'); + return; + } + + $scope.rawContent = angular.copy(parsed); + if (($scope.rawContent instanceof Array) && ($scope.rawContent.length > 0)) { + if ($scope.rawContent.length > 1) $scope.isMultiRealm = true; + $scope.fileContent = $scope.rawContent[0]; + } else { + $scope.fileContent = $scope.rawContent; + } + + $scope.importing = true; + setOnOffSwitchDefaults(); + $scope.results = {}; + if (!$scope.hasResources()) { + $scope.nothingToImport(); + } + }; + + $scope.hasResults = function() { + return (Object.keys($scope.results).length > 0) && + ($scope.results.results !== undefined) && + ($scope.results.results.length > 0); + } + + $scope.resultsPage = function() { + if (!$scope.hasResults()) return {}; + return $scope.results.results.slice(startIndex(), endIndex()); + } + + function startIndex() { + return pageSize * $scope.currentPage; + } + + function endIndex() { + var length = $scope.results.results.length; + var endIndex = startIndex() + pageSize; + if (endIndex > length) endIndex = length; + return endIndex; + } + + function setOnOffSwitchDefaults() { + $scope.importUsers = $scope.hasArray('users'); + $scope.importGroups = $scope.hasArray('groups'); + $scope.importClients = $scope.hasArray('clients'); + $scope.importIdentityProviders = $scope.hasArray('identityProviders'); + $scope.importRealmRoles = $scope.hasRealmRoles(); + $scope.importClientRoles = $scope.hasClientRoles(); + } + + $scope.setFirstPage = function() { + $scope.currentPage = 0; + } + + $scope.setNextPage = function() { + $scope.currentPage++; + } + + $scope.setPreviousPage = function() { + $scope.currentPage--; + } + + $scope.hasNext = function() { + if (!$scope.hasResults()) return false; + var length = $scope.results.results.length; + //console.log('length=' + length); + var endIndex = startIndex() + pageSize; + //console.log('endIndex=' + endIndex); + return length > endIndex; + } + + $scope.hasPrevious = function() { + if (!$scope.hasResults()) return false; + return $scope.currentPage > 0; + } + + $scope.viewImportDetails = function() { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/view-object.html', + controller: 'ObjectModalCtrl', + resolve: { + object: function () { + return $scope.fileContent; + } + } + }) + }; + + $scope.hasArray = function(section) { + return ($scope.fileContent !== 'undefined') && + ($scope.fileContent.hasOwnProperty(section)) && + ($scope.fileContent[section] instanceof Array) && + ($scope.fileContent[section].length > 0); + } + + $scope.hasRealmRoles = function() { + return $scope.hasRoles() && + ($scope.fileContent.roles.hasOwnProperty('realm')) && + ($scope.fileContent.roles.realm instanceof Array) && + ($scope.fileContent.roles.realm.length > 0); + } + + $scope.hasRoles = function() { + return ($scope.fileContent !== 'undefined') && + ($scope.fileContent.hasOwnProperty('roles')) && + ($scope.fileContent.roles !== 'undefined'); + } + + $scope.hasClientRoles = function() { + return $scope.hasRoles() && + ($scope.fileContent.roles.hasOwnProperty('client')) && + (Object.keys($scope.fileContent.roles.client).length > 0); + } + + $scope.itemCount = function(section) { + if (!$scope.importing) return 0; + if ($scope.hasRealmRoles() && (section === 'roles.realm')) return $scope.fileContent.roles.realm.length; + if ($scope.hasClientRoles() && (section === 'roles.client')) return clientRolesCount($scope.fileContent.roles.client); + + if (!$scope.fileContent.hasOwnProperty(section)) return 0; + + return $scope.fileContent[section].length; + } + + clientRolesCount = function(clientRoles) { + var total = 0; + for (var clientName in clientRoles) { + total += clientRoles[clientName].length; + } + return total; + } + + $scope.hasResources = function() { + return ($scope.importUsers && $scope.hasArray('users')) || + ($scope.importGroups && $scope.hasArray('groups')) || + ($scope.importClients && $scope.hasArray('clients')) || + ($scope.importIdentityProviders && $scope.hasArray('identityProviders')) || + ($scope.importRealmRoles && $scope.hasRealmRoles()) || + ($scope.importClientRoles && $scope.hasClientRoles()); + } + + $scope.nothingToImport = function() { + Notifications.error('No resources specified to import.'); + } + + $scope.$watch('fileContent', function() { + if (!angular.equals($scope.fileContent, oldCopy)) { + $scope.changed = true; + } + setOnOffSwitchDefaults(); + }, true); + + $scope.successMessage = function() { + var message = $scope.results.added + ' records added. '; + if ($scope.ifResourceExists === 'SKIP') { + message += $scope.results.skipped + ' records skipped.' + } + if ($scope.ifResourceExists === 'OVERWRITE') { + message += $scope.results.overwritten + ' records overwritten.'; + } + return message; + } + + $scope.save = function() { + var json = angular.copy($scope.fileContent); + json.ifResourceExists = $scope.ifResourceExists; + if (!$scope.importUsers) delete json.users; + if (!$scope.importGroups) delete json.groups; + if (!$scope.importIdentityProviders) delete json.identityProviders; + if (!$scope.importClients) delete json.clients; + + if (json.hasOwnProperty('roles')) { + if (!$scope.importRealmRoles) delete json.roles.realm; + if (!$scope.importClientRoles) delete json.roles.client; + } + + var importFile = $resource(authUrl + '/admin/realms/' + realm.realm + '/partialImport'); + $scope.results = importFile.save(json, function() { + Notifications.success($scope.successMessage()); + }, function(error) { + if (error.data.errorMessage) { + Notifications.error(error.data.errorMessage); + } else { + Notifications.error('Unexpected error during import'); + } + }); + }; + + $scope.reset = function() { + $route.reload(); + } + +}); + +module.controller('RealmExportCtrl', function($scope, realm, $http, + $httpParamSerializer, Notifications, Dialog) { + $scope.realm = realm; + $scope.exportGroupsAndRoles = false; + $scope.exportClients = false; + + $scope.export = function() { + if ($scope.exportGroupsAndRoles || $scope.exportClients) { + Dialog.confirm('Export', 'This operation may make server unresponsive for a while.\n\nAre you sure you want to proceed?', download); + } else { + download(); + } + } + + function download() { + var exportUrl = authUrl + '/admin/realms/' + realm.realm + '/partial-export'; + var params = {}; + if ($scope.exportGroupsAndRoles) { + params['exportGroupsAndRoles'] = true; + } + if ($scope.exportClients) { + params['exportClients'] = true; + } + if (Object.keys(params).length > 0) { + exportUrl += '?' + $httpParamSerializer(params); + } + $http.post(exportUrl) + .then(function(response) { + var download = angular.fromJson(response.data); + download = angular.toJson(download, true); + saveAs(new Blob([download], { type: 'application/json' }), 'realm-export.json'); + }).catch(function() { + Notifications.error("Sorry, something went wrong."); + }); + } +}); diff --git a/juice/welcome/resources/juice-bc.svg b/juice/welcome/resources/juice-bc.svg new file mode 100644 index 0000000..3c65812 --- /dev/null +++ b/juice/welcome/resources/juice-bc.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/juice/welcome/resources/juice.svg b/juice/welcome/resources/juice.svg new file mode 100644 index 0000000..ee1ec0d --- /dev/null +++ b/juice/welcome/resources/juice.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/juice/welcome/resources/linkedin.svg b/juice/welcome/resources/linkedin.svg new file mode 100644 index 0000000..e74895b --- /dev/null +++ b/juice/welcome/resources/linkedin.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/welcome/resources/twitter.svg b/juice/welcome/resources/twitter.svg new file mode 100644 index 0000000..a6b0441 --- /dev/null +++ b/juice/welcome/resources/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/juice/welcome/theme.properties b/juice/welcome/theme.properties new file mode 100644 index 0000000..805c36a --- /dev/null +++ b/juice/welcome/theme.properties @@ -0,0 +1,11 @@ +styles=css/welcome.css css/styles.css +import=common/keycloak + +stylesCommon=node_modules/patternfly/dist/css/patternfly.css node_modules/patternfly/dist/css/patternfly-additions.css +scripts=realm.js +signInUrl=/auth/realms/demo/account +displayCommunityLinks=true + +welcomeTitle=Welcome + +locales=ca,cs,da,de,en,es,fr,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN diff --git a/restart.sh b/restart.sh new file mode 100755 index 0000000..46e17f5 --- /dev/null +++ b/restart.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +docker build -t juice-keycloak . + +docker stop juice-keycloak || true + +docker run --rm -it -d \ + -p 8080:8080 \ + -e KEYCLOAK_USER=admin \ + -e KEYCLOAK_PASSWORD=admin \ + -e DB_VENDOR=postgres \ + -e DB_ADDR=host.docker.internal \ + -e DB_DATABASE=keycloak \ + -e DB_USER=keycloak \ + -e DB_PASSWORD=keycloak \ + --name juice-keycloak \ + juice-keycloak \ No newline at end of file diff --git a/test/url.test.js b/test/url.test.js new file mode 100644 index 0000000..17d1a01 --- /dev/null +++ b/test/url.test.js @@ -0,0 +1,110 @@ +// installable packages +const base64url = require("base64url"); +const { v4: uuidv4 } = require("uuid"); +const fetch = require("node-fetch").default; + +// native packages +const crypto = require("crypto"); +const { URL, URLSearchParams } = require("url"); + +// config +const BASE_URL = "http://localhost:8080"; +const REALM = "demorealm"; +const REDIRECT_URI = "com.juice.booster3://auth"; +const CLIENT_ID = "native-webview"; + +const sha256encrypt = (code) => { + const base64Digest = crypto + .createHash("sha256") + .update(code) + .digest("base64"); + + return base64url.fromBase64(base64Digest); +}; + +const nonceFactory = () => { + const timestamp = Date.now(); + return sha256encrypt(timestamp.toString()); +}; + +function generateAuthUrl() { + const nonce = uuidv4(); + const state = uuidv4(); + + const url = new URL( + `${BASE_URL}/auth/realms/${REALM}/protocol/openid-connect/auth`, + ); + url.searchParams.append("client_id", CLIENT_ID); + url.searchParams.append("redirect_uri", REDIRECT_URI); + url.searchParams.append("state", state); + url.searchParams.append("response_mode", "fragment"); + url.searchParams.append("response_type", "code"); + url.searchParams.append("scope", "openid"); + url.searchParams.append("nonce", nonce); + + console.log(url.href); +} + +async function getTokenByUrl(urlString) { + let url = new URL(urlString); + + let code = new URLSearchParams(url.hash).get("code"); + console.log(`Auth with code is: ${code}`); + + url = new URL( + `${BASE_URL}/auth/realms/${REALM}/protocol/openid-connect/token`, + ); + const payload = { + client_id: CLIENT_ID, + code, + grant_type: "authorization_code", + redirect_uri: REDIRECT_URI, + }; + + let response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "*/*", + }, + redirect: "manual", + body: new URLSearchParams(payload).toString(), + }); + + const token = await response.json(); + console.log(token); +} + +async function refresh(refresh_token) { + const url = new URL( + `${BASE_URL}/auth/realms/${REALM}/protocol/openid-connect/token`, + ); + const payload = { + client_id: CLIENT_ID, + refresh_token, + grant_type: "refresh_token", + redirect_uri: REDIRECT_URI, + }; + + let response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "*/*", + }, + redirect: "manual", + body: new URLSearchParams(payload).toString(), + }); + + const token = await response.json(); + console.log(token); +} + +if (require.main === module) { + const redirectUri = process.argv[2]; + if (redirectUri == null) { + generateAuthUrl(); + } else { + getTokenByUrl(redirectUri); + } +} diff --git a/version.ini b/version.ini new file mode 100644 index 0000000..6c6aa7c --- /dev/null +++ b/version.ini @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file