Initial commit
No files matched your search
@@ -0,0 +1,49 @@
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### Kotlin ###
|
||||
.kotlin
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
/node_modules/
|
||||
|
||||
/.local.yaml
|
||||
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -0,0 +1 @@
|
||||
welcome1
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,7 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="default">
|
||||
<module2copyright>
|
||||
<element module="Project Files" copyright="default" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
</component>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="0@172.16.51.17" uuid="ef12f723-6f4c-4952-a1a5-c0d2aa237e72">
|
||||
<driver-ref>redis</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>jdbc.RedisDriver</jdbc-driver>
|
||||
<jdbc-url>jdbc:redis://172.16.51.17:6379/0</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="liberica-25" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,43 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="myValues">
|
||||
<value>
|
||||
<list size="6">
|
||||
<item index="0" class="java.lang.String" itemvalue="x-data" />
|
||||
<item index="1" class="java.lang.String" itemvalue="@htmx:after-request.camel" />
|
||||
<item index="2" class="java.lang.String" itemvalue="x-on:turnstile:ready.window" />
|
||||
<item index="3" class="java.lang.String" itemvalue="x-on:before-swap.camel" />
|
||||
<item index="4" class="java.lang.String" itemvalue="x-on:after-swap.camel" />
|
||||
<item index="5" class="java.lang.String" itemvalue="x-on:after-request.camel" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myCustomValuesEnabled" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="myValues">
|
||||
<value>
|
||||
<list size="7">
|
||||
<item index="0" class="java.lang.String" itemvalue="nobr" />
|
||||
<item index="1" class="java.lang.String" itemvalue="noembed" />
|
||||
<item index="2" class="java.lang.String" itemvalue="comment" />
|
||||
<item index="3" class="java.lang.String" itemvalue="noscript" />
|
||||
<item index="4" class="java.lang.String" itemvalue="embed" />
|
||||
<item index="5" class="java.lang.String" itemvalue="script" />
|
||||
<item index="6" class="java.lang.String" itemvalue="link" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myCustomValuesEnabled" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="customHeaders">
|
||||
<set>
|
||||
<option value="HX-Redirect" />
|
||||
</set>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="externalSystemId" value="Gradle" />
|
||||
<option name="version" value="2.4.21" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectResources">
|
||||
<resource url="http://www.ultraq.net.nz/thymeleaf/layout" location="$USER_HOME$/.gradle/caches/modules-2/files-2.1/nz.net.ultraq.thymeleaf/thymeleaf-layout-dialect/4.0.1/9510c824484045cdfdd98c39d374ec94ac379cc3/thymeleaf-layout-dialect-4.0.1.jar!/nz/net/ultraq/thymeleaf/layoutdialect/LayoutDialect.xml" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="liberica-25" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,165 @@
|
||||
import com.github.gradle.node.npm.task.NpmTask
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
import org.gradle.language.jvm.tasks.ProcessResources
|
||||
import org.springframework.boot.buildpack.platform.build.PullPolicy
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||
import org.springframework.boot.gradle.tasks.run.BootRun
|
||||
|
||||
plugins {
|
||||
id("org.springframework.boot") version "4.1.1"
|
||||
id("io.spring.dependency-management") version "1.1.7"
|
||||
id("com.github.node-gradle.node") version "7.1.0"
|
||||
|
||||
kotlin("jvm") version "2.4.21"
|
||||
kotlin("plugin.spring") version "2.4.21"
|
||||
|
||||
application
|
||||
java
|
||||
}
|
||||
|
||||
group = "com.redinterglobal"
|
||||
version = "1.3.4-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = uri("https://git.redinterglobal.com/api/packages/scastro/maven") }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
||||
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
||||
implementation("org.springframework.boot:spring-boot-starter-jackson")
|
||||
implementation("org.springframework.boot:spring-boot-starter-cache")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-redis")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-mail")
|
||||
|
||||
implementation("com.digitalsanctuary:ds-spring-cf-turnstile:2.1.0")
|
||||
|
||||
implementation("com.github.ben-manes.caffeine:caffeine")
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:8.0.4")
|
||||
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
|
||||
implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:4.0.1")
|
||||
|
||||
implementation("io.github.wimdeblauwe:htmx-spring-boot-thymeleaf:6.0.1") {
|
||||
exclude(group = "org.springframework.boot")
|
||||
}
|
||||
|
||||
implementation("com.redinterglobal:thymepack:1.52-SNAPSHOT")
|
||||
implementation("com.redinterglobal:sdolib:1.6.6-SNAPSHOT")
|
||||
|
||||
implementation(platform("org.zalando:logbook-bom:4.2.0"))
|
||||
implementation("org.zalando:logbook-spring-boot-starter")
|
||||
implementation("org.zalando:logbook-core")
|
||||
implementation("org.zalando:logbook-spring")
|
||||
implementation("org.zalando:logbook-json")
|
||||
|
||||
implementation(kotlin("reflect"))
|
||||
implementation("tools.jackson.module:jackson-module-kotlin")
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("org.springframework.security:spring-security-test")
|
||||
testImplementation("org.springframework:spring-test")
|
||||
testImplementation("org.mockito:mockito-core")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(25)
|
||||
}
|
||||
|
||||
tasks.named<Test>("test") {
|
||||
useJUnitPlatform()
|
||||
dependsOn(processPageResources)
|
||||
classpath += files(generatedResourcesDir)
|
||||
}
|
||||
|
||||
node {
|
||||
download = true
|
||||
version = "24.21.0"
|
||||
}
|
||||
|
||||
val npmRunBuild = tasks.register<NpmTask>("npmRunBuild") {
|
||||
description = "Build webpack"
|
||||
args = listOf("run", "build")
|
||||
dependsOn("npmInstall")
|
||||
|
||||
inputs.files(fileTree("node_modules"))
|
||||
inputs.files(fileTree("src/main/resources/js"))
|
||||
inputs.files(fileTree("src/main/resources/styles"))
|
||||
inputs.files(fileTree("src/main/resources/templates"))
|
||||
inputs.file("package.json")
|
||||
inputs.file("webpack.config.js")
|
||||
outputs.dir(layout.buildDirectory.dir("resources/main/static/dist"))
|
||||
}
|
||||
|
||||
val generatedResourcesDir = layout.buildDirectory.dir("generated-resources/main")
|
||||
|
||||
val processPageResources = tasks.register<Copy>("processPageResources") {
|
||||
from("src/main/resources/templates") {
|
||||
into("templates")
|
||||
}
|
||||
from("src/main/resources/static") {
|
||||
into("static")
|
||||
}
|
||||
into(generatedResourcesDir)
|
||||
}
|
||||
|
||||
tasks.named<ProcessResources>("processResources") {
|
||||
exclude("templates/**", "js/**", "styles/**", "static/**")
|
||||
}
|
||||
|
||||
npmRunBuild.configure {
|
||||
mustRunAfter(tasks.named("processResources"))
|
||||
}
|
||||
|
||||
tasks.named<BootJar>("bootJar") {
|
||||
dependsOn(npmRunBuild, processPageResources)
|
||||
from(generatedResourcesDir) {
|
||||
into("BOOT-INF/classes")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<Jar>("jar") {
|
||||
dependsOn(npmRunBuild, processPageResources)
|
||||
from(generatedResourcesDir)
|
||||
}
|
||||
|
||||
tasks.named("resolveMainClassName") {
|
||||
dependsOn(npmRunBuild)
|
||||
}
|
||||
|
||||
tasks.named<BootRun>("bootRun") {
|
||||
dependsOn(npmRunBuild, processPageResources)
|
||||
classpath += files(generatedResourcesDir)
|
||||
}
|
||||
|
||||
|
||||
tasks.named<BootBuildImage>("bootBuildImage") {
|
||||
|
||||
imagePlatform = "linux/amd64"
|
||||
builder = "paketobuildpacks/builder-noble-java-tiny:0.0.199"
|
||||
pullPolicy = PullPolicy.ALWAYS
|
||||
environment.put("BP_JVM_VERSION", "25")
|
||||
|
||||
val repo = "git.redinterglobal.com/scastro/${project.name}"
|
||||
imageName = "$repo:${project.version}"
|
||||
tags = listOf("$repo:latest")
|
||||
|
||||
publish = true
|
||||
docker {
|
||||
publishRegistry {
|
||||
url = "https://git.redinterglobal.com"
|
||||
username = providers.gradleProperty("owner")
|
||||
password = providers.gradleProperty("access_token")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
kotlin.code.style=official
|
||||
org.gradle.configuration-cache=true
|
||||
@@ -0,0 +1,9 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,112 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
@rem Catch executions from older scripts and ensure they exit cleanly.
|
||||
@rem This can be removed once we can be reasonably confident that few people
|
||||
@rem will be migrating directly to this new wrapper.
|
||||
goto afterSafetyNet
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
goto exitWithErrorLevel
|
||||
:afterSafetyNet
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
1>&2 echo.
|
||||
1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
1>&2 echo.
|
||||
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
|
||||
1>&2 echo location of your Java installation.
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto exitWithErrorLevel
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
1>&2 echo.
|
||||
1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
1>&2 echo.
|
||||
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
|
||||
1>&2 echo location of your Java installation.
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto exitWithErrorLevel
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
|
||||
|
||||
@rem This label must not be changed. We rely on old scripts being able to jump to this point.
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "welcome1",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"license": "ISC",
|
||||
"author": "",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"devserver": "webpack serve --open --mode development",
|
||||
"watch": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fullhuman/postcss-purgecss": "^7.0.2",
|
||||
"autoprefixer": "^10.6.1",
|
||||
"chunks-webpack-plugin": "^13.1.0",
|
||||
"css-loader": "^7.1.5",
|
||||
"css-minimizer-webpack-plugin": "^8.0.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"postcss-loader": "^8.2.1",
|
||||
"sass": "^1.105.1",
|
||||
"sass-loader": "^17.0.1",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"style-loader": "^4.0.0",
|
||||
"webpack": "^5.111.1",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.8.0",
|
||||
"@pqina/flip": "^1.8.4",
|
||||
"@qr-platform/qr-code.js": "^0.20.14",
|
||||
"@tanstack/hotkeys": "^0.17.0",
|
||||
"alpinejs": "^3.17.5",
|
||||
"bootstrap": "^6.0.0-alpha.1",
|
||||
"datatables.net-dt": "^3.1.4",
|
||||
"htmx.org": "4.0.0",
|
||||
"moment": "^2.31.0",
|
||||
"party-js": "^2.2.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"vanilla-calendar-pro": "^3.4.0"
|
||||
},
|
||||
"allowScripts": {
|
||||
"fsevents@2.3.3": true,
|
||||
"@parcel/watcher@2.6.0": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||
}
|
||||
rootProject.name = "welcome1"
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome
|
||||
|
||||
import com.redinterglobal.welcome.config.WelcomeConfig
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cache.annotation.EnableCaching
|
||||
import org.springframework.scheduling.annotation.EnableAsync
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
@EnableCaching
|
||||
@EnableConfigurationProperties(WelcomeConfig::class)
|
||||
class WelcomeApp
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<WelcomeApp>(*args)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.config
|
||||
|
||||
import com.digitalsanctuary.cf.turnstile.config.TurnstileConfigProperties
|
||||
import com.digitalsanctuary.cf.turnstile.metrics.TurnstileMetrics
|
||||
import com.digitalsanctuary.cf.turnstile.service.TurnstileValidationService
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.web.client.RestClient
|
||||
|
||||
class DevelTurnstileValidationService(
|
||||
properties: TurnstileConfigProperties,
|
||||
metrics: TurnstileMetrics
|
||||
) : TurnstileValidationService(RestClient.create(), properties, metrics) {
|
||||
|
||||
override fun validateTurnstileResponse(token: String?, remoteIp: String?): Boolean = true
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Profile("devel")
|
||||
class DevelTurnstileConfiguration {
|
||||
|
||||
@Bean
|
||||
fun turnstileValidationService(
|
||||
properties: TurnstileConfigProperties,
|
||||
metrics: TurnstileMetrics
|
||||
): TurnstileValidationService = DevelTurnstileValidationService(properties, metrics)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.config
|
||||
|
||||
import jakarta.servlet.http.Cookie
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class MonitorAuthenticationSuccessHandler : SavedRequestAwareAuthenticationSuccessHandler() {
|
||||
|
||||
override fun onAuthenticationSuccess(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authentication: Authentication
|
||||
) {
|
||||
val email = (authentication.principal as? OAuth2User)
|
||||
?.getAttribute<String>("email")
|
||||
?.trim()
|
||||
?.takeIf(String::isNotBlank)
|
||||
|
||||
if (email != null) {
|
||||
response.addCookie(
|
||||
Cookie(COOKIE_NAME, email).apply {
|
||||
path = "/"
|
||||
maxAge = COOKIE_MAX_AGE_SECONDS
|
||||
isHttpOnly = true
|
||||
secure = request.isSecure
|
||||
setAttribute("SameSite", "Lax")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
super.onAuthenticationSuccess(request, response, authentication)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val COOKIE_NAME = "monitor-email"
|
||||
const val COOKIE_MAX_AGE_SECONDS = 30 * 24 * 60 * 60
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.config
|
||||
|
||||
import com.digitalsanctuary.cf.turnstile.service.TurnstileValidationService
|
||||
import io.github.wimdeblauwe.htmx.spring.boot.security.HxRedirectToPageAccessDeniedHandler
|
||||
import io.github.wimdeblauwe.htmx.spring.boot.security.HxRedirectToPageAuthenticationEntryPoint
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties
|
||||
import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientPropertiesMapper
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(OAuth2ClientProperties::class)
|
||||
class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
fun unmappedControllerRequestMatcher(
|
||||
requestMappingHandlerMapping: RequestMappingHandlerMapping
|
||||
) = UnmappedControllerRequestMatcher(requestMappingHandlerMapping)
|
||||
|
||||
@Bean("monitorClientRegistrationRepository")
|
||||
fun monitorClientRegistrationRepository(
|
||||
properties: OAuth2ClientProperties
|
||||
): ClientRegistrationRepository {
|
||||
val registrations = OAuth2ClientPropertiesMapper(properties).asClientRegistrations()
|
||||
return InMemoryClientRegistrationRepository(registrations.values.toList())
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun monitorLoginHintAuthorizationRequestResolver(
|
||||
@Qualifier("monitorClientRegistrationRepository") monitorClientRegistrationRepository: ClientRegistrationRepository
|
||||
): OAuth2AuthorizationRequestResolver =
|
||||
MonitorLoginHintAuthorizationRequestResolver(monitorClientRegistrationRepository)
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
fun welcomeMonitorSecurityConfiguration(
|
||||
http: HttpSecurity,
|
||||
@Qualifier("monitorClientRegistrationRepository") monitorClientRegistrationRepository: ClientRegistrationRepository,
|
||||
monitorLoginHintAuthorizationRequestResolver: OAuth2AuthorizationRequestResolver,
|
||||
unmappedControllerRequestMatcher: UnmappedControllerRequestMatcher,
|
||||
monitorAuthenticationSuccessHandler: MonitorAuthenticationSuccessHandler
|
||||
): SecurityFilterChain {
|
||||
http {
|
||||
securityMatcher("/monitor", "/monitor/**", "/oauth2/**", "/login/oauth2/**")
|
||||
authorizeHttpRequests {
|
||||
authorize(unmappedControllerRequestMatcher, permitAll)
|
||||
authorize("/monitor/login", permitAll)
|
||||
authorize("/monitor/login/select", permitAll)
|
||||
authorize(anyRequest, hasRole("MONITOR"))
|
||||
}
|
||||
}
|
||||
http.oauth2Login { oauth2Login ->
|
||||
oauth2Login
|
||||
.loginPage("/monitor/login")
|
||||
.clientRegistrationRepository(monitorClientRegistrationRepository)
|
||||
.authorizationEndpoint { authorizationEndpoint ->
|
||||
authorizationEndpoint.authorizationRequestResolver(monitorLoginHintAuthorizationRequestResolver)
|
||||
}
|
||||
.successHandler(monitorAuthenticationSuccessHandler)
|
||||
.userInfoEndpoint { userInfoEndpoint ->
|
||||
userInfoEndpoint.userAuthoritiesMapper(monitorAuthoritiesMapper())
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun monitorAuthoritiesMapper(): GrantedAuthoritiesMapper =
|
||||
GrantedAuthoritiesMapper { authorities ->
|
||||
authorities.toSet() + SimpleGrantedAuthority("ROLE_MONITOR")
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(2)
|
||||
fun welcomeAppConfiguration(
|
||||
http: HttpSecurity,
|
||||
turnstileAuthenticationProvider: TurnstileAuthenticationProvider,
|
||||
turnstileValidationService: TurnstileValidationService,
|
||||
loginAuthenticationFailureHandler: LoginAuthenticationFailureHandler,
|
||||
unmappedControllerRequestMatcher: UnmappedControllerRequestMatcher
|
||||
): SecurityFilterChain {
|
||||
val invitationSuccessHandler = InvitationAuthenticationSuccessHandler("/")
|
||||
http.authenticationProvider(turnstileAuthenticationProvider)
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(unmappedControllerRequestMatcher, permitAll)
|
||||
authorize("/error/**", permitAll)
|
||||
authorize("/css/**", permitAll)
|
||||
authorize("/js/**", permitAll)
|
||||
authorize("/dist/**", permitAll)
|
||||
authorize("/img/**", permitAll)
|
||||
authorize("/favicon/**", permitAll)
|
||||
authorize("/site.webmanifest", permitAll)
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin {
|
||||
permitAll()
|
||||
authenticationFailureHandler = loginAuthenticationFailureHandler
|
||||
authenticationSuccessHandler = invitationSuccessHandler
|
||||
loginPage = "/login"
|
||||
loginProcessingUrl = "/login"
|
||||
usernameParameter = "email"
|
||||
passwordParameter = "cf-turnstile-response"
|
||||
authenticationDetailsSource = ClientIpAuthenticationDetailsSource(turnstileValidationService)
|
||||
failureUrl = "/login?error"
|
||||
}
|
||||
exceptionHandling {
|
||||
authenticationEntryPoint = HxRedirectToPageAuthenticationEntryPoint(
|
||||
LoginUrlAuthenticationEntryPoint("/login")
|
||||
)
|
||||
accessDeniedHandler = HxRedirectToPageAccessDeniedHandler()
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
|
||||
internal data class PendingMonitorLoginHint(
|
||||
val registrationId: String,
|
||||
val email: String
|
||||
)
|
||||
|
||||
internal class MonitorLoginHintAuthorizationRequestResolver(
|
||||
clientRegistrationRepository: ClientRegistrationRepository
|
||||
) : OAuth2AuthorizationRequestResolver {
|
||||
|
||||
private val delegate = DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository)
|
||||
|
||||
override fun resolve(request: HttpServletRequest): OAuth2AuthorizationRequest? =
|
||||
addLoginHint(request, delegate.resolve(request))
|
||||
|
||||
override fun resolve(
|
||||
request: HttpServletRequest,
|
||||
clientRegistrationId: String
|
||||
): OAuth2AuthorizationRequest? = addLoginHint(request, delegate.resolve(request, clientRegistrationId))
|
||||
|
||||
private fun addLoginHint(
|
||||
request: HttpServletRequest,
|
||||
authorizationRequest: OAuth2AuthorizationRequest?
|
||||
): OAuth2AuthorizationRequest? {
|
||||
val pendingHint = request.getSession(false)
|
||||
?.getAttribute(PENDING_LOGIN_HINT_SESSION_ATTRIBUTE) as? PendingMonitorLoginHint
|
||||
?: return authorizationRequest
|
||||
|
||||
val registrationId = authorizationRequest
|
||||
?.attributes
|
||||
?.get(OAuth2ParameterNames.REGISTRATION_ID)
|
||||
?.toString()
|
||||
?: return authorizationRequest
|
||||
|
||||
if (pendingHint.registrationId != registrationId) {
|
||||
return authorizationRequest
|
||||
}
|
||||
|
||||
request.session.removeAttribute(PENDING_LOGIN_HINT_SESSION_ATTRIBUTE)
|
||||
val loginHint = pendingHint.email.trim().takeIf(String::isNotBlank)
|
||||
?: return authorizationRequest
|
||||
|
||||
return OAuth2AuthorizationRequest.from(authorizationRequest)
|
||||
.additionalParameters { parameters -> parameters[LOGIN_HINT_PARAMETER] = loginHint }
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PENDING_LOGIN_HINT_SESSION_ATTRIBUTE = "monitor.login.hint"
|
||||
|
||||
private const val LOGIN_HINT_PARAMETER = "login_hint"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.config
|
||||
|
||||
import com.digitalsanctuary.cf.turnstile.service.TurnstileValidationService
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import com.redinterglobal.sdo.domain.User
|
||||
import com.redinterglobal.sdo.domain.UserStatus
|
||||
import com.redinterglobal.welcome.service.SdoWelcomeService
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.context.annotation.Primary
|
||||
import org.springframework.context.MessageSource
|
||||
import org.springframework.context.i18n.LocaleContextHolder
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource
|
||||
import org.springframework.security.authentication.AuthenticationProvider
|
||||
import org.springframework.security.authentication.BadCredentialsException
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.authority.AuthorityUtils
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler
|
||||
import org.springframework.security.web.DefaultRedirectStrategy
|
||||
import org.springframework.security.web.RedirectStrategy
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
data class TurnstileLoginDetails(val clientIpAddress: String)
|
||||
|
||||
data class SdoAuthenticationPrincipal(
|
||||
val user: User,
|
||||
val registrationId: String
|
||||
)
|
||||
|
||||
class ClientIpAuthenticationDetailsSource(
|
||||
private val turnstileValidationService: TurnstileValidationService
|
||||
) : AuthenticationDetailsSource<HttpServletRequest, TurnstileLoginDetails> {
|
||||
|
||||
override fun buildDetails(context: HttpServletRequest): TurnstileLoginDetails =
|
||||
TurnstileLoginDetails(turnstileValidationService.getClientIpAddress(context))
|
||||
}
|
||||
|
||||
@Component
|
||||
class TurnstileAuthenticationProvider(
|
||||
private val turnstileValidationService: TurnstileValidationService,
|
||||
private val sdoWelcomeService: SdoWelcomeService
|
||||
) : AuthenticationProvider {
|
||||
|
||||
override fun authenticate(authentication: Authentication): Authentication {
|
||||
val email = authentication.name.trim()
|
||||
val turnstileResponse = authentication.credentials?.toString().orEmpty()
|
||||
val clientIpAddress = (authentication.details as? TurnstileLoginDetails)?.clientIpAddress
|
||||
|
||||
if (email.isBlank() ||
|
||||
!turnstileValidationService.validateTurnstileResponse(turnstileResponse, clientIpAddress)
|
||||
) {
|
||||
throw BadCredentialsException("Login verification failed")
|
||||
}
|
||||
|
||||
val account = try {
|
||||
sdoWelcomeService.getSdoUser(email)
|
||||
} catch (_: RuntimeException) {
|
||||
throw BadCredentialsException("Login verification failed")
|
||||
}
|
||||
|
||||
if (account.user.status == UserStatus.LOCKED) {
|
||||
throw BadCredentialsException("User account is locked")
|
||||
}
|
||||
|
||||
val principal = SdoAuthenticationPrincipal(account.user, account.registrationId)
|
||||
return UsernamePasswordAuthenticationToken(
|
||||
principal,
|
||||
null,
|
||||
AuthorityUtils.createAuthorityList("ROLE_USER")
|
||||
).also { it.details = authentication.details }
|
||||
}
|
||||
|
||||
override fun supports(authentication: Class<*>): Boolean =
|
||||
UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication)
|
||||
}
|
||||
|
||||
@Component
|
||||
class LoginAuthenticationFailureHandler(
|
||||
private val messageSource: MessageSource,
|
||||
private val objectMapper: ObjectMapper,
|
||||
) : AuthenticationFailureHandler {
|
||||
|
||||
private val redirectStrategy: RedirectStrategy = DefaultRedirectStrategy()
|
||||
|
||||
override fun onAuthenticationFailure(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
exception: org.springframework.security.core.AuthenticationException,
|
||||
) {
|
||||
if (request.getHeader("HX-Request") == null) {
|
||||
redirectStrategy.sendRedirect(request, response, "/login?error")
|
||||
return
|
||||
}
|
||||
|
||||
val message = messageSource.getMessage("login.error", null, LocaleContextHolder.getLocale())
|
||||
val trigger = objectMapper.writeValueAsString(
|
||||
mapOf("flash" to mapOf("target" to "#flash", "message" to message))
|
||||
)
|
||||
response.status = HttpServletResponse.SC_UNAUTHORIZED
|
||||
response.setHeader("HX-Reswap", "none")
|
||||
response.setHeader("HX-Trigger", trigger)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class InvitationAuthenticationSuccessHandler(private val redirect: String) : AuthenticationSuccessHandler {
|
||||
|
||||
private val redirectHandler = SimpleUrlAuthenticationSuccessHandler(redirect)
|
||||
|
||||
override fun onAuthenticationSuccess(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authentication: Authentication
|
||||
) {
|
||||
val principal = authentication.principal as SdoAuthenticationPrincipal
|
||||
request.session.setAttribute("sdoUser", principal.user)
|
||||
request.session.setAttribute("registrationId", principal.registrationId)
|
||||
|
||||
if (request.getHeader("HX-Request") != null) {
|
||||
response.setHeader("HX-Redirect", redirect)
|
||||
response.status = HttpServletResponse.SC_OK
|
||||
return
|
||||
}
|
||||
|
||||
redirectHandler.onAuthenticationSuccess(request, response, authentication)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.config
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
|
||||
|
||||
/**
|
||||
* Lets requests with no MVC controller mapping reach Spring Boot's error handling.
|
||||
*
|
||||
* This keeps valid application routes protected while allowing anonymous visitors to
|
||||
* receive an HTTP 404 page for a mistyped or obsolete URL.
|
||||
*/
|
||||
class UnmappedControllerRequestMatcher(
|
||||
private val handlerMapping: RequestMappingHandlerMapping
|
||||
) : RequestMatcher {
|
||||
|
||||
override fun matches(request: HttpServletRequest): Boolean =
|
||||
handlerMapping.getHandler(request) == null
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.config
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
|
||||
data class SdoServer(
|
||||
val registrationId: String,
|
||||
val group: String? = null,
|
||||
)
|
||||
|
||||
data class AppStore(
|
||||
val logo: String,
|
||||
val link: String,
|
||||
val os: String,
|
||||
val version: String
|
||||
)
|
||||
|
||||
@ConfigurationProperties(prefix = "welcome")
|
||||
class WelcomeConfig(
|
||||
val otpDuration: Int = 180,
|
||||
val appStores: List<AppStore>,
|
||||
val shortLink: String? = null,
|
||||
val sdoDomainServers: Map<String, SdoServer> = emptyMap(),
|
||||
val idp: Map<String, String> = emptyMap()
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.controller
|
||||
|
||||
import com.redinterglobal.welcome.config.SdoAuthenticationPrincipal
|
||||
import com.redinterglobal.welcome.service.SdoService
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import java.security.Principal
|
||||
|
||||
@Controller
|
||||
class CustomerLogoController(
|
||||
private val sdoService: SdoService
|
||||
) {
|
||||
@GetMapping("/logo", "/monitor/logo")
|
||||
fun logo(principal: Principal): ResponseEntity<ByteArray> {
|
||||
|
||||
val registrationId = if (principal is UsernamePasswordAuthenticationToken && principal.principal is SdoAuthenticationPrincipal) {
|
||||
val user = principal.principal as SdoAuthenticationPrincipal
|
||||
user.registrationId
|
||||
} else if (principal is OAuth2AuthenticationToken) {
|
||||
principal.principal.getAttribute("registrationId")
|
||||
} else null
|
||||
|
||||
val logo = if (registrationId != null) sdoService.logo(registrationId) else null
|
||||
return if (logo != null) ResponseEntity.ok().contentType(logo.mediaType).body(logo.media) else ResponseEntity.notFound().build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.controller
|
||||
|
||||
import com.redinterglobal.welcome.config.SdoAuthenticationPrincipal
|
||||
import com.redinterglobal.welcome.service.OtpService
|
||||
import com.redinterglobal.welcome.service.SdoService
|
||||
import com.redinterglobal.welcome.service.StorageService
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.github.wimdeblauwe.htmx.spring.boot.mvc.HxRequest
|
||||
import jakarta.servlet.http.HttpSession
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.SessionAttribute
|
||||
import org.springframework.web.bind.annotation.SessionAttributes
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Controller
|
||||
@SessionAttributes("devicesCount")
|
||||
class InvitationController(
|
||||
private val sdoService: SdoService,
|
||||
private val storageService: StorageService
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val INVITATION_ID = "invitationId"
|
||||
}
|
||||
|
||||
@GetMapping(params = ["step=3"])
|
||||
fun getEnrolment(@AuthenticationPrincipal sdoUser: SdoAuthenticationPrincipal): String {
|
||||
logger.info { "Step 3. ${sdoUser.user.displayName} <${sdoUser.user.email}>"}
|
||||
return "enroll"
|
||||
}
|
||||
|
||||
@HxRequest
|
||||
@GetMapping("/enroll/invite")
|
||||
fun getEnrolment(@AuthenticationPrincipal sdoUser: SdoAuthenticationPrincipal,
|
||||
@SessionAttribute(value = OtpService.OTP_DEMO_KEY, required = false) demo: String?,
|
||||
model: Model, session: HttpSession
|
||||
):String {
|
||||
storageService.upsert(sdoUser.user, "enroll")
|
||||
if (demo == null) {
|
||||
val invitationId = sdoService.createSingleInvitation(sdoUser.registrationId, sdoUser.user.id).invitation.id
|
||||
val invitationUrl =
|
||||
sdoService.getInvitation(sdoUser.registrationId, sdoUser.user.id, invitationId)!!.enrollmentUrl
|
||||
session.setAttribute(INVITATION_ID, invitationId)
|
||||
model.addAttribute("invitationLink", invitationUrl)
|
||||
} else {
|
||||
model.addAttribute("invitationLink", "https://doubleoctopus.com")
|
||||
model.addAttribute("demo", demo)
|
||||
}
|
||||
return "enroll :: invite"
|
||||
}
|
||||
|
||||
@GetMapping("/enroll/status")
|
||||
fun getEnrolmentStatus(
|
||||
@AuthenticationPrincipal sdoUser: SdoAuthenticationPrincipal,
|
||||
@SessionAttribute(name = "invitationId", required = false) invitationId: String?,
|
||||
@SessionAttribute(name = OtpService.OTP_DEMO_KEY, required = false) demo: String?): String {
|
||||
if (demo == null) {
|
||||
requireNotNull(invitationId)
|
||||
val invitation = sdoService.getInvitation(sdoUser.registrationId, sdoUser.user.id, invitationId)
|
||||
if (invitation != null) {
|
||||
logger.debug { "Pooling invitation $invitationId status: ${invitation.invitationStatus}" }
|
||||
return "enroll :: waiting"
|
||||
} else {
|
||||
logger.debug { "Invitation $invitationId not found" }
|
||||
storageService.upsert(sdoUser.user, "enrolled")
|
||||
}
|
||||
}
|
||||
return "redirect:htmx:/?step=4"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.controller
|
||||
|
||||
import com.digitalsanctuary.cf.turnstile.service.TurnstileValidationService
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.SessionAttributes
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
|
||||
@Controller
|
||||
@SessionAttributes("devicesCount")
|
||||
class LoginController(
|
||||
private val turnstileValidationService: TurnstileValidationService,
|
||||
private val environment: Environment
|
||||
) {
|
||||
|
||||
@GetMapping("/login")
|
||||
fun getLogin(
|
||||
model: Model,
|
||||
@RequestParam(required = false) error: String?
|
||||
): String {
|
||||
model.addAttribute("turnstileSiteKey", turnstileValidationService.turnstileSitekey)
|
||||
model.addAttribute("turnstileEnabled", !environment.activeProfiles.contains("devel"))
|
||||
model.addAttribute("loginError", error != null)
|
||||
return "login"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.controller
|
||||
|
||||
import com.digitalsanctuary.cf.turnstile.service.TurnstileValidationService
|
||||
import com.redinterglobal.welcome.config.MonitorAuthenticationSuccessHandler
|
||||
import com.redinterglobal.welcome.config.MonitorLoginHintAuthorizationRequestResolver
|
||||
import com.redinterglobal.welcome.config.PendingMonitorLoginHint
|
||||
import com.redinterglobal.welcome.config.WelcomeConfig
|
||||
import com.redinterglobal.welcome.domain.EnrolledUser
|
||||
import com.redinterglobal.welcome.domain.EnrolledUserRepo
|
||||
import com.redinterglobal.welcome.service.EnrolledUserSseRegistry
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.CookieValue
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.ResponseBody
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes
|
||||
import jakarta.servlet.http.HttpSession
|
||||
import kotlin.collections.contains
|
||||
|
||||
@Controller
|
||||
class MonitorController(
|
||||
private val enrolledUserRepo: EnrolledUserRepo,
|
||||
private val enrolledUserSseRegistry: EnrolledUserSseRegistry,
|
||||
private val turnstileValidationService: TurnstileValidationService,
|
||||
private val environment: Environment,
|
||||
private val welcomeConfig: WelcomeConfig
|
||||
) {
|
||||
|
||||
@GetMapping("/monitor/login")
|
||||
fun login(
|
||||
model: Model,
|
||||
@RequestParam(required = false) error: String?,
|
||||
@CookieValue(name = MonitorAuthenticationSuccessHandler.COOKIE_NAME, required = false) monitorEmail: String?
|
||||
): String {
|
||||
model.addAttribute("turnstileSiteKey", turnstileValidationService.turnstileSitekey)
|
||||
model.addAttribute("turnstileEnabled", !environment.activeProfiles.contains("devel"))
|
||||
model.addAttribute("loginError", error != null)
|
||||
model.addAttribute("monitorEmail", monitorEmail)
|
||||
return "monitor/login"
|
||||
}
|
||||
|
||||
@PostMapping("/monitor/login/select")
|
||||
fun handleLogin(
|
||||
@RequestParam("email") email: String,
|
||||
redirectAttributes: RedirectAttributes,
|
||||
session: HttpSession
|
||||
): String {
|
||||
|
||||
session.removeAttribute(MonitorLoginHintAuthorizationRequestResolver.PENDING_LOGIN_HINT_SESSION_ATTRIBUTE)
|
||||
|
||||
val domain = getDomain(email)
|
||||
if (domain == null) {
|
||||
redirectAttributes.addAttribute("error", "Invalid email")
|
||||
return "redirect:htmx:/monitor/login"
|
||||
}
|
||||
|
||||
val registrationId = welcomeConfig.idp[domain]
|
||||
if (registrationId == null) {
|
||||
redirectAttributes.addAttribute("error", "Invalid domain")
|
||||
return "redirect:htmx:/monitor/login"
|
||||
}
|
||||
|
||||
session.setAttribute(
|
||||
MonitorLoginHintAuthorizationRequestResolver.PENDING_LOGIN_HINT_SESSION_ATTRIBUTE,
|
||||
PendingMonitorLoginHint("${registrationId}-idP", email)
|
||||
)
|
||||
|
||||
return "redirect:htmx:/oauth2/authorization/${registrationId}-idP"
|
||||
}
|
||||
|
||||
private fun getDomain(username: String): String? {
|
||||
val user = username.lowercase()
|
||||
val at = user.lastIndexOf("@")
|
||||
return if (at < 0 || at == user.length - 1) {
|
||||
null
|
||||
} else user.substring(at + 1)
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping("/monitor/users", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun getUserList(): List<EnrolledUser> = enrolledUserRepo.findAll().filterNotNull()
|
||||
|
||||
@GetMapping("/monitor")
|
||||
fun getAdminPage(model: Model): String {
|
||||
return "monitor/monitor"
|
||||
}
|
||||
|
||||
@GetMapping("/monitor/sse", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
|
||||
fun streamSse(@AuthenticationPrincipal principal: OAuth2User): SseEmitter {
|
||||
val email = principal.getAttribute<String>("email") ?: principal.name
|
||||
val domain = email.substringAfter("@")
|
||||
return enrolledUserSseRegistry.subscribe(domain)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.controller
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.servlet.ModelAndView
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException
|
||||
|
||||
@ControllerAdvice(annotations = [Controller::class])
|
||||
class NotFoundExceptionHandler {
|
||||
|
||||
@ExceptionHandler(NoResourceFoundException::class)
|
||||
fun handleNotFound(): ModelAndView = ModelAndView("error/404").apply {
|
||||
status = HttpStatus.NOT_FOUND
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.controller
|
||||
|
||||
import com.redinterglobal.welcome.config.SdoAuthenticationPrincipal
|
||||
import com.redinterglobal.welcome.service.IEmailService
|
||||
import com.redinterglobal.welcome.service.OtpService
|
||||
import com.redinterglobal.welcome.service.OtpService.Companion.OTP_DEMO_KEY
|
||||
import com.redinterglobal.welcome.service.StorageService
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponse
|
||||
import io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxReswap
|
||||
import io.github.wimdeblauwe.htmx.spring.boot.mvc.HxRequest
|
||||
import jakarta.servlet.http.HttpSession
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.servlet.ModelAndView
|
||||
import kotlin.time.Clock
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Controller
|
||||
@RequestMapping("", "/")
|
||||
class OtpController(
|
||||
private val otpService: OtpService,
|
||||
private val emailService: IEmailService,
|
||||
private val storageService: StorageService
|
||||
) {
|
||||
|
||||
companion object {
|
||||
internal const val OTP_COUNT_KEY = "otpCount"
|
||||
private const val MAX_OTP_FAILURES = 3
|
||||
private val OTP_PATTERN = Regex("[0-9]{6}")
|
||||
}
|
||||
|
||||
@GetMapping(params = ["step=2"])
|
||||
fun otpPage(@AuthenticationPrincipal sdoUser: SdoAuthenticationPrincipal): String {
|
||||
storageService.upsert(sdoUser.user, "otp")
|
||||
return "otp"
|
||||
}
|
||||
|
||||
@GetMapping("/otp")
|
||||
fun handleOtpTimer(model: Model,
|
||||
session: HttpSession,
|
||||
@AuthenticationPrincipal sdoUser: SdoAuthenticationPrincipal
|
||||
): String {
|
||||
val existingOtp = otpService.getOtp(sdoUser.user.email!!)
|
||||
if (existingOtp.otp != 0) {
|
||||
val duration = (existingOtp.expiry - Clock.System.now())
|
||||
model.addAttribute("otpExpirySeconds", duration.inWholeSeconds.coerceAtLeast(0))
|
||||
} else {
|
||||
otpService.generateOtp(sdoUser.user.email!!).run {
|
||||
emailService.sendOtpEmail(sdoUser.user.email!!, sdoUser.user.displayName?:"", this)
|
||||
}
|
||||
session.removeAttribute(OTP_COUNT_KEY)
|
||||
model.addAttribute("otpExpirySeconds", otpService.duration)
|
||||
}
|
||||
return "otp :: timer"
|
||||
}
|
||||
|
||||
@HxRequest
|
||||
@PostMapping("/otp")
|
||||
fun handleOtpSubmit(
|
||||
@AuthenticationPrincipal sdoUser: SdoAuthenticationPrincipal,
|
||||
@RequestParam(required = false) otp: String?,
|
||||
session: HttpSession,
|
||||
htmxResponse: HtmxResponse,
|
||||
): Any {
|
||||
val email = sdoUser.user.email!!
|
||||
val failures = session.getAttribute(OTP_COUNT_KEY) as? Int ?: 0
|
||||
if (failures >= MAX_OTP_FAILURES) {
|
||||
throw OtpAttemptLimitException()
|
||||
}
|
||||
val submittedOtp = otp?.takeIf(OTP_PATTERN::matches)?.toIntOrNull()
|
||||
val activeOtp = otpService.getOtp(email)
|
||||
|
||||
if (submittedOtp == OtpService.OTP_DEMO) {
|
||||
logger.warn { "OTP demo used for email: $email" }
|
||||
session.setAttribute(OTP_DEMO_KEY, OTP_DEMO_KEY)
|
||||
return "redirect:htmx:/?step=3"
|
||||
} else {
|
||||
session.removeAttribute(OTP_DEMO_KEY)
|
||||
}
|
||||
|
||||
if (activeOtp.expiry <= Clock.System.now()) {
|
||||
htmxResponse.setReswap(HtmxReswap.none())
|
||||
htmxResponse.addTrigger("otp-expired")
|
||||
return ResponseEntity.status(HttpStatus.GONE).build<Void>()
|
||||
}
|
||||
|
||||
if (submittedOtp == null || activeOtp.otp != submittedOtp) {
|
||||
session.setAttribute(OTP_COUNT_KEY, failures + 1)
|
||||
return failure(HttpStatus.UNPROCESSABLE_CONTENT, "Invalid OTP", htmxResponse)
|
||||
}
|
||||
|
||||
session.removeAttribute(OTP_COUNT_KEY)
|
||||
return "redirect:htmx:/?step=3"
|
||||
}
|
||||
|
||||
private fun failure(
|
||||
status: HttpStatus,
|
||||
message: String,
|
||||
htmxResponse: HtmxResponse,
|
||||
): ResponseEntity<Void> {
|
||||
htmxResponse.setReswap(HtmxReswap.none())
|
||||
htmxResponse.addTrigger(
|
||||
"flash",
|
||||
mapOf("target" to "#flash", "level" to "warning", "message" to message),
|
||||
)
|
||||
return ResponseEntity.status(status).build()
|
||||
}
|
||||
|
||||
@ExceptionHandler(OtpAttemptLimitException::class)
|
||||
fun otpAttemptLimit(): ModelAndView = ModelAndView("otp :: otpLocked").apply {
|
||||
status = HttpStatus.TOO_MANY_REQUESTS
|
||||
}
|
||||
|
||||
@HxRequest
|
||||
@PostMapping("/otp/resend")
|
||||
fun resendOtp(
|
||||
@AuthenticationPrincipal sdoUser: SdoAuthenticationPrincipal,
|
||||
model: Model,
|
||||
session: HttpSession,
|
||||
): String {
|
||||
val email = sdoUser.user.email!!
|
||||
val otp = otpService.generateOtp(email)
|
||||
emailService.sendOtpEmail(email, sdoUser.user.displayName ?: "", otp)
|
||||
session.removeAttribute(OTP_COUNT_KEY)
|
||||
model.addAttribute("otpExpirySeconds", otpService.duration)
|
||||
return "otp :: timer"
|
||||
}
|
||||
|
||||
@HxRequest
|
||||
@GetMapping("/otpExpired")
|
||||
fun otpExpired() = "otp :: otpExpired"
|
||||
|
||||
@HxRequest
|
||||
@GetMapping("/otpRestart")
|
||||
fun otpRestart() = "redirect:htmx:/otp"
|
||||
|
||||
@HxRequest
|
||||
@GetMapping("/otpAccepted")
|
||||
fun otpAccepted() = "otp :: otpAccepted"
|
||||
|
||||
}
|
||||
|
||||
internal class OtpAttemptLimitException : RuntimeException()
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.controller
|
||||
|
||||
import com.redinterglobal.sdo.SdoClient
|
||||
import com.redinterglobal.welcome.config.SdoAuthenticationPrincipal
|
||||
import com.redinterglobal.welcome.service.StorageService
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.github.wimdeblauwe.htmx.spring.boot.mvc.HxRequest
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Controller
|
||||
class PushController(
|
||||
private val sdoClient: SdoClient,
|
||||
private val storageService: StorageService
|
||||
) {
|
||||
|
||||
@GetMapping(params = ["step=4"])
|
||||
fun getPushTest(@AuthenticationPrincipal sdoUser: SdoAuthenticationPrincipal): String {
|
||||
logger.info { "Step 4. ${sdoUser.user.displayName} <${sdoUser.user.email}>"}
|
||||
storageService.upsert(sdoUser.user, "enrolled")
|
||||
return "push"
|
||||
}
|
||||
|
||||
@HxRequest
|
||||
@GetMapping("/push/now")
|
||||
fun pushNow(
|
||||
@AuthenticationPrincipal sdoUser: SdoAuthenticationPrincipal): String {
|
||||
storageService.upsert(sdoUser.user, "push")
|
||||
sdoClient.stateVerify(sdoUser.registrationId, sdoUser.user.id)
|
||||
storageService.upsert(sdoUser.user, "tested")
|
||||
return "redirect:htmx:/done"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.controller
|
||||
|
||||
import com.redinterglobal.welcome.config.SdoAuthenticationPrincipal
|
||||
import com.redinterglobal.welcome.config.WelcomeConfig
|
||||
import com.redinterglobal.welcome.service.StorageService
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/", "")
|
||||
class WelcomeController(
|
||||
private val welcomeConfig: WelcomeConfig,
|
||||
private val storageService: StorageService
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
fun showDownload(model: Model,
|
||||
@AuthenticationPrincipal sdoUser: SdoAuthenticationPrincipal): String {
|
||||
logger.info { "Step 1. ${sdoUser.user.displayName} <${sdoUser.user.email}>"}
|
||||
model.addAttribute("apps", welcomeConfig.appStores)
|
||||
model.addAttribute("shortLink", welcomeConfig.shortLink)
|
||||
model.addAttribute("nextStep", "2")
|
||||
storageService.upsert(sdoUser.user, "download")
|
||||
return "download"
|
||||
}
|
||||
|
||||
@GetMapping("/done")
|
||||
fun done() = "done"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.domain
|
||||
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.redis.core.RedisHash
|
||||
import org.springframework.data.redis.core.index.Indexed
|
||||
import org.springframework.data.repository.ListCrudRepository
|
||||
import kotlin.time.Clock
|
||||
|
||||
@RedisHash(value = "enrolledUsers", timeToLive = 3600)
|
||||
class EnrolledUser private constructor(
|
||||
@Id
|
||||
var email: String,
|
||||
val name: String?,
|
||||
val step: String,
|
||||
@Indexed
|
||||
val domain: String,
|
||||
val added: Long
|
||||
) {
|
||||
constructor(email: String, name: String?, step: String) : this(email, name, step, email.substringAfter("@"),
|
||||
Clock.System.now().epochSeconds
|
||||
)
|
||||
val duration: Long
|
||||
get() = Clock.System.now().epochSeconds - added
|
||||
}
|
||||
|
||||
interface EnrolledUserRepo: ListCrudRepository<EnrolledUser, String> {
|
||||
fun findAllByDomain(domain: String): List<EnrolledUser>
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.service
|
||||
|
||||
import com.redinterglobal.welcome.domain.EnrolledUser
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.scheduling.annotation.Async
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class EnrolledUserListener(
|
||||
private val enrolledUserSseRegistry: EnrolledUserSseRegistry
|
||||
) {
|
||||
|
||||
@Async
|
||||
@EventListener
|
||||
fun onEnrolledUser(enrolledUser: EnrolledUser) {
|
||||
enrolledUserSseRegistry.broadcast(enrolledUser)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.service
|
||||
|
||||
import com.redinterglobal.welcome.domain.EnrolledUser
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Service
|
||||
class EnrolledUserSseRegistry {
|
||||
|
||||
private val emittersByDomain = ConcurrentHashMap<String, MutableSet<SseEmitter>>()
|
||||
|
||||
fun subscribe(domain: String): SseEmitter = register(domain, SseEmitter(0L))
|
||||
|
||||
fun broadcast(enrolledUser: EnrolledUser) {
|
||||
emittersByDomain[enrolledUser.domain]?.toList()?.forEach { emitter ->
|
||||
try {
|
||||
emitter.send(
|
||||
SseEmitter.event()
|
||||
.name("enrolled")
|
||||
.data(enrolledUser)
|
||||
)
|
||||
} catch (exception: IOException) {
|
||||
remove(enrolledUser.domain, emitter)
|
||||
emitter.completeWithError(exception)
|
||||
} catch (exception: IllegalStateException) {
|
||||
remove(enrolledUser.domain, emitter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun register(domain: String, emitter: SseEmitter): SseEmitter {
|
||||
emittersByDomain.computeIfAbsent(domain) { ConcurrentHashMap.newKeySet() }.add(emitter)
|
||||
|
||||
val cleanup = { remove(domain, emitter) }
|
||||
emitter.onCompletion(cleanup)
|
||||
emitter.onTimeout(cleanup)
|
||||
emitter.onError { cleanup() }
|
||||
|
||||
return emitter
|
||||
}
|
||||
|
||||
private fun remove(domain: String, emitter: SseEmitter) {
|
||||
emittersByDomain[domain]?.let { emitters ->
|
||||
emitters.remove(emitter)
|
||||
if (emitters.isEmpty()) {
|
||||
emittersByDomain.remove(domain, emitters)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.service
|
||||
|
||||
import org.springframework.http.MediaType
|
||||
|
||||
data class Logo(
|
||||
val mediaType: MediaType,
|
||||
val media: ByteArray
|
||||
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Logo
|
||||
|
||||
if (mediaType != other.mediaType) return false
|
||||
if (!media.contentEquals(other.media)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = mediaType.hashCode()
|
||||
result = 31 * result + media.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.service
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.mail.SimpleMailMessage
|
||||
import org.springframework.mail.javamail.JavaMailSender
|
||||
import org.springframework.mail.javamail.MimeMessageHelper
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.stereotype.Service
|
||||
import org.thymeleaf.TemplateEngine
|
||||
import org.thymeleaf.context.Context
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
data class EmailDetails (
|
||||
val recipient: String,
|
||||
val subject: String,
|
||||
val msgBody: String,
|
||||
)
|
||||
|
||||
interface IEmailService {
|
||||
fun sendSimpleEmail(details: EmailDetails): Boolean
|
||||
fun sendMailWithAttachment(details: EmailDetails): Boolean
|
||||
fun sendOtpEmail(recipient: String, name: String, otp: Int): Boolean
|
||||
}
|
||||
|
||||
@Service
|
||||
@Profile("!devel")
|
||||
class EmailService(
|
||||
private val javaMailSender: JavaMailSender,
|
||||
@Qualifier("emailTemplateEngine") private val emailTemplateEngine: TemplateEngine,
|
||||
@Value($$"${spring.mail.sender}") private val sender: String,
|
||||
): IEmailService {
|
||||
|
||||
override fun sendSimpleEmail(details: EmailDetails): Boolean {
|
||||
logger.info { "Sending email to ${details.recipient}" }
|
||||
try {
|
||||
SimpleMailMessage().apply {
|
||||
from = sender
|
||||
setTo(details.recipient)
|
||||
text = details.msgBody
|
||||
subject = details.subject
|
||||
}.run {
|
||||
javaMailSender.send(this)
|
||||
}
|
||||
return true
|
||||
}
|
||||
catch (e: Exception) {
|
||||
logger.error { "Failed to send email to ${details.recipient}: ${e.message}" }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendMailWithAttachment(details: EmailDetails): Boolean {
|
||||
val htmlContent = ""
|
||||
val mimeMessage = javaMailSender.createMimeMessage()
|
||||
MimeMessageHelper(mimeMessage, true, "UTF-8").apply {
|
||||
setFrom(sender)
|
||||
setTo(details.recipient)
|
||||
setSubject(details.subject)
|
||||
setText(htmlContent, true)
|
||||
}
|
||||
javaMailSender.send(mimeMessage)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun sendOtpEmail(recipient: String, name: String, otp: Int): Boolean {
|
||||
logger.info { "Sending OTP email to $recipient" }
|
||||
return try {
|
||||
val htmlContent = emailTemplateEngine.process(
|
||||
"mail/otp",
|
||||
Context().apply {
|
||||
setVariable("name", name)
|
||||
setVariable("otp", otp)
|
||||
},
|
||||
)
|
||||
val mimeMessage = javaMailSender.createMimeMessage()
|
||||
MimeMessageHelper(mimeMessage, false, "UTF-8").apply {
|
||||
setFrom(sender)
|
||||
setTo(recipient)
|
||||
setSubject("Código de verificación")
|
||||
setText(htmlContent, true)
|
||||
}
|
||||
javaMailSender.send(mimeMessage)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Failed to send OTP email to $recipient" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Service
|
||||
@Profile("devel")
|
||||
class DummyEmailService : IEmailService {
|
||||
|
||||
override fun sendSimpleEmail(details: EmailDetails) = true
|
||||
|
||||
override fun sendMailWithAttachment(details: EmailDetails) = true
|
||||
|
||||
override fun sendOtpEmail(recipient: String, name: String, otp: Int): Boolean {
|
||||
logger.info { "Development OTP for $recipient: $otp" }
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.service
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.redinterglobal.welcome.config.WelcomeConfig
|
||||
import org.springframework.stereotype.Service
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.Instant
|
||||
|
||||
data class Otp(
|
||||
val otp: Int,
|
||||
val expiry: Instant
|
||||
)
|
||||
|
||||
@Service
|
||||
class OtpService(
|
||||
welcomeConfig: WelcomeConfig
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val OTP_DEMO = 123456
|
||||
const val OTP_DEMO_KEY = "demo"
|
||||
}
|
||||
|
||||
private val secureRandom = SecureRandom()
|
||||
val duration = welcomeConfig.otpDuration
|
||||
|
||||
private val otpCache =
|
||||
Caffeine.newBuilder()
|
||||
.expireAfterWrite(duration.toLong(), TimeUnit.SECONDS)
|
||||
.build<String, Otp> { Otp(0, Instant.DISTANT_PAST) }
|
||||
|
||||
private fun random() = 100_000 + secureRandom.nextInt(900_000)
|
||||
|
||||
fun generateOtp(key: String): Int {
|
||||
var otp: Int
|
||||
do { otp = random() } while (otp == OTP_DEMO)
|
||||
otpCache.put(key, Otp(otp, Clock.System.now() + duration.seconds))
|
||||
return otp
|
||||
}
|
||||
|
||||
fun getOtp(key: String): Otp {
|
||||
return otpCache.get(key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.service
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.redinterglobal.sdo.DIRECTORIES_EXPLORER_MEMBERS_FILTER
|
||||
import com.redinterglobal.sdo.SdoClient
|
||||
import com.redinterglobal.sdo.domain.Invitation
|
||||
import com.redinterglobal.sdo.domain.InvitationRequest
|
||||
import com.redinterglobal.sdo.domain.InvitationResult
|
||||
import com.redinterglobal.sdo.domain.InvitationType
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.cache.annotation.Caching
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.io.encoding.Base64
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class SdoService(
|
||||
private val sdoClient: SdoClient
|
||||
) {
|
||||
|
||||
val logoCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.HOURS)
|
||||
.build { registrationId: String ->
|
||||
val base64img = sdoClient.getGeneralSettings(registrationId).logo
|
||||
if (!base64img.startsWith("data:image/")) {
|
||||
logger.warn { "Invalid logo format $registrationId" }
|
||||
return@build null
|
||||
}
|
||||
val (header, media) = base64img.split(",", limit = 2)
|
||||
val mediaType = header.substringAfter(":").substringBefore(";")
|
||||
Logo(MediaType.valueOf(mediaType), Base64.decode(media))
|
||||
}
|
||||
|
||||
@Caching
|
||||
fun groupId(registrationId: String, groupName: String): Long? {
|
||||
val groups = sdoClient.directoriesMembersSearch(
|
||||
registrationId = registrationId,
|
||||
search = groupName,
|
||||
filter = DIRECTORIES_EXPLORER_MEMBERS_FILTER.GROUPS
|
||||
).data
|
||||
if (groups.isEmpty()) return null
|
||||
return groups.singleOrNull{ it.displayName == groupName }?.id
|
||||
}
|
||||
|
||||
fun createSingleInvitation(registrationId: String, userId: Long): InvitationResult {
|
||||
sdoClient.getInvitations(registrationId, userId).filter { it.type == InvitationType.OCTOPUS }
|
||||
.forEach { sdoClient.deleteUserInvitation(registrationId, userId, it.id) }
|
||||
return sdoClient.postInvitation(registrationId, userId, InvitationRequest(InvitationType.OCTOPUS, false))
|
||||
}
|
||||
|
||||
fun getInvitation(registrationId: String, userId: Long, invitationId: String): Invitation? {
|
||||
return sdoClient.getInvitations(registrationId, userId).singleOrNull { it.id == invitationId }
|
||||
}
|
||||
|
||||
fun logo(registrationId: String) = logoCache.get(registrationId)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.service
|
||||
|
||||
import com.redinterglobal.sdo.DIRECTORIES_EXPLORER_MEMBERS_FILTER
|
||||
import com.redinterglobal.sdo.SdoClient
|
||||
import com.redinterglobal.sdo.domain.User
|
||||
import com.redinterglobal.welcome.config.WelcomeConfig
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
data class SdoAccount(
|
||||
val email: String,
|
||||
val registrationId: String,
|
||||
val user: User
|
||||
)
|
||||
|
||||
@Service
|
||||
class SdoWelcomeService(
|
||||
private val welcomeConfig: WelcomeConfig,
|
||||
private val sdoClient: SdoClient,
|
||||
private val sdoService: SdoService
|
||||
) {
|
||||
|
||||
fun getSdoUser(email: String): SdoAccount {
|
||||
val domain = email.substringAfter("@")
|
||||
val server = welcomeConfig.sdoDomainServers[domain.lowercase()]!!
|
||||
val registrationId = server.registrationId
|
||||
val user = (if (server.group != null) {
|
||||
sdoService.groupId(registrationId, server.group)?.let { groupId ->
|
||||
sdoClient.getGroupMembers(registrationId, groupId, search = email)
|
||||
}
|
||||
} else {
|
||||
sdoClient.directoriesMembersSearch(
|
||||
registrationId = registrationId,
|
||||
search = email,
|
||||
filter = DIRECTORIES_EXPLORER_MEMBERS_FILTER.USERS
|
||||
)
|
||||
})?.data?.singleOrNull{ it.email.equals(email, ignoreCase = true) }?: throw ResponseStatusException(
|
||||
HttpStatus.NOT_FOUND,
|
||||
"Yor email address is not entitled to use this service."
|
||||
)
|
||||
return SdoAccount(email, registrationId, user)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.service
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.thymeleaf.TemplateEngine
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine
|
||||
import org.thymeleaf.templatemode.TemplateMode
|
||||
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
|
||||
import org.thymeleaf.templateresolver.ITemplateResolver
|
||||
|
||||
@Configuration
|
||||
class SpringMailConfig {
|
||||
|
||||
@Bean
|
||||
fun emailTemplateEngine(): TemplateEngine = SpringTemplateEngine().apply {
|
||||
addTemplateResolver(htmlTemplateResolver())
|
||||
}
|
||||
|
||||
private fun htmlTemplateResolver(): ITemplateResolver {
|
||||
return ClassLoaderTemplateResolver().apply {
|
||||
order = 1
|
||||
resolvablePatterns = mutableSetOf("mail/*")
|
||||
prefix = ""
|
||||
suffix = ".html"
|
||||
templateMode = TemplateMode.HTML
|
||||
characterEncoding = "UTF-8"
|
||||
isCacheable = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
package com.redinterglobal.welcome.service
|
||||
|
||||
import com.redinterglobal.sdo.domain.User
|
||||
import com.redinterglobal.welcome.domain.EnrolledUser
|
||||
import com.redinterglobal.welcome.domain.EnrolledUserRepo
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class StorageService(
|
||||
private val enrolledUserRepo: EnrolledUserRepo,
|
||||
private val eventPublisher: ApplicationEventPublisher
|
||||
)
|
||||
{
|
||||
fun upsert(user: User, status: String) {
|
||||
val enrolledUser = EnrolledUser(user.email!!, user.displayName, status)
|
||||
enrolledUserRepo.save(enrolledUser)
|
||||
eventPublisher.publishEvent(enrolledUser)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
spring:
|
||||
thymeleaf:
|
||||
# Read templates from the working tree so edits are visible without an IDE
|
||||
# resource copy and without restarting the application.
|
||||
prefix: file:src/main/resources/templates/
|
||||
cache: false
|
||||
devtools:
|
||||
restart:
|
||||
# IntelliJ may still copy a changed template to the runtime classpath.
|
||||
# Do not treat that resource-only change as a JVM restart trigger.
|
||||
additional-exclude: templates/**
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.redinterglobal.welcome: INFO
|
||||
org.zalando.logbook: INFO
|
||||
com.redinterglobal.thymepack: WARN
|
||||
|
||||
logbook:
|
||||
format:
|
||||
style: http
|
||||
filter:
|
||||
form-request-mode: PARAMETER
|
||||
obfuscate:
|
||||
json-body-fields:
|
||||
- token
|
||||
headers:
|
||||
- token
|
||||
- authorization
|
||||
|
||||
server:
|
||||
forward-headers-strategy: framework
|
||||
|
||||
ds:
|
||||
cf:
|
||||
turnstile:
|
||||
sitekey: ${cf.sitekey}
|
||||
secret: ${cf.secret}
|
||||
|
||||
spring:
|
||||
config:
|
||||
import:
|
||||
- optional:file:.local.yaml
|
||||
|
||||
messages:
|
||||
fallback-to-system-locale: false
|
||||
|
||||
threads:
|
||||
virtual:
|
||||
enabled: true
|
||||
@@ -0,0 +1,8 @@
|
||||
________ __________________
|
||||
___ __ \_____ _________________ ____________________ /__ ____/________________
|
||||
__ /_/ / __ `/_ ___/_ ___/_ | /| / / __ \_ ___/ __ /__ /_ __ ___/ _ \ _ \
|
||||
_ ____// /_/ /_(__ )_(__ )__ |/ |/ // /_/ / / / /_/ / _ __/ _ / / __/ __/
|
||||
/_/ \__,_/ /____/ /____/ ____/|__/ \____//_/ \__,_/ /_/ /_/ \___/\___/
|
||||
|
||||
${application.title} ${application.version}
|
||||
Powered by Spring Boot ${spring-boot.version}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
.tick {
|
||||
font-size:1.75rem; white-space:nowrap; font-family:arial,sans-serif
|
||||
}
|
||||
|
||||
.tick-flip,.tick-text-inline {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
|
||||
.tick-label {
|
||||
margin-top:1em;font-size:1em;
|
||||
}
|
||||
|
||||
.tick-char {
|
||||
width:1.5em;
|
||||
}
|
||||
|
||||
.tick-text-inline {
|
||||
display:inline-block;text-align:center;min-width:1em;
|
||||
}
|
||||
|
||||
.tick-text-inline+.tick-text-inline {
|
||||
margin-left:-.325em;
|
||||
}
|
||||
|
||||
.tick-group {
|
||||
margin:0 .5em;text-align:center;
|
||||
}
|
||||
|
||||
.tick-text-inline {
|
||||
color: rgb(90, 93, 99) !important;
|
||||
}
|
||||
|
||||
.tick-label {
|
||||
color: rgb(90, 93, 99) !important;
|
||||
}
|
||||
|
||||
.tick-flip-panel {
|
||||
color: rgb(255, 255, 255) !important;
|
||||
}
|
||||
|
||||
.tick-flip {
|
||||
font-family: !important;
|
||||
}
|
||||
|
||||
.tick-flip-panel-text-wrapper {
|
||||
line-height: 1.45 !important;
|
||||
}
|
||||
|
||||
.tick-flip-panel {
|
||||
background-color: rgb(59, 61, 59) !important;
|
||||
}
|
||||
|
||||
.tick-flip {
|
||||
border-radius:0.12em !important;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
import '../styles/styles.scss';
|
||||
import htmx from "htmx.org";
|
||||
import 'htmx.org/dist/ext/hx-live.js'
|
||||
import party from 'party-js';
|
||||
|
||||
window.party = party;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
import '../styles/styles.scss'
|
||||
import { initQrCode } from './qrcode.js'
|
||||
import htmx from 'htmx.org';
|
||||
|
||||
window.initQrCode = initQrCode
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
import '../styles/styles.scss'
|
||||
import htmx from 'htmx.org';
|
||||
import { HotkeyManager } from '@tanstack/hotkeys';
|
||||
import { initQrCode } from './qrcode.js'
|
||||
|
||||
HotkeyManager.getInstance().register(['s', 'd', 'o'], () => {
|
||||
console.log('sdo!')
|
||||
htmx.trigger(document.body, 'demo');
|
||||
});
|
||||
|
||||
window.initQrCode = initQrCode;
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
import '../styles/styles.scss'
|
||||
|
||||
import htmx from 'htmx.org';
|
||||
window.htmx = htmx;
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
Alpine.start();
|
||||
|
||||
import './turnstile-htmx.js';
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
import '../styles/styles.scss';
|
||||
import '../styles/datatables.scss';
|
||||
|
||||
import htmx from 'htmx.org';
|
||||
import 'htmx.org/dist/ext/hx-sse.js'
|
||||
|
||||
import DataTable from 'datatables.net-dt';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
window.htmx = htmx;
|
||||
|
||||
|
||||
let table = new DataTable('#table', {
|
||||
responsive: true,
|
||||
ajax: {
|
||||
url: '/monitor/users',
|
||||
dataSrc: ''
|
||||
},
|
||||
columns: [
|
||||
{ data: 'name' },
|
||||
{ data: 'email' },
|
||||
{
|
||||
data: 'step',
|
||||
render: function(data,type) {
|
||||
if (type==='display') {
|
||||
if (data==='download') {
|
||||
return '<span class="badge badge-subtle theme-danger">Download</span>';
|
||||
} else if (data==='otp') {
|
||||
return '<span class="badge badge-subtle theme-warning">OTP</span>';
|
||||
} else if (data==='enroll') {
|
||||
return '<span class="badge theme-warning">Enroll</span>';
|
||||
} else if (data==='enrolled') {
|
||||
return '<span class="badge badge-subtle theme-success">Enrolled</span>';
|
||||
} else if (data==='push') {
|
||||
return '<span class="badge badge-subtle theme-success">Enrolled</span>'+
|
||||
'<span class="badge badge-subtle theme-info">Notification</span>';
|
||||
} else if (data==='tested') {
|
||||
return '<span class="badge badge-subtle theme-success">Enrolled</span>'+
|
||||
'<span class="badge badge-subtle theme-success">Tested</span>';
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'duration',
|
||||
render: function(data,type) {
|
||||
if (type==='display') {
|
||||
return moment.duration(data, 'seconds').humanize();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
});
|
||||
|
||||
htmx.on('htmx:sse:after:message', event => {
|
||||
console.log('updated', event.detail);
|
||||
table.ajax.reload();
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
import '../styles/styles.scss';
|
||||
import '@pqina/flip/dist/flip.min.css';
|
||||
import '../css/tick.css';
|
||||
import htmx from 'htmx.org';
|
||||
import 'htmx.org/dist/ext/hx-live.js'
|
||||
import * as bootstrap from 'bootstrap'
|
||||
import Tick from '@pqina/flip';
|
||||
|
||||
import { rootSelector } from './util.js';
|
||||
|
||||
window.bootstrap = bootstrap;
|
||||
|
||||
document.body.addEventListener('htmx:after:swap', (evt) => {
|
||||
|
||||
rootSelector(evt, '.tick', (el) => {
|
||||
const tick = Tick.DOM.create(el);
|
||||
const offset = new Date();
|
||||
const seconds = el.dataset.otpExpiry;
|
||||
const deadline = new Date(offset.setMilliseconds(offset.getMilliseconds() + (seconds * 1000)));
|
||||
const counter = Tick.count.down(deadline, {format: ['m', 's']});
|
||||
counter.onupdate = function(value) {
|
||||
tick.value = value;
|
||||
}
|
||||
counter.onended = function() {
|
||||
htmx.trigger('#otp-expired', 'otp-expired');
|
||||
}
|
||||
})
|
||||
|
||||
rootSelector(evt, '.otp', (el) => {
|
||||
el.otp = bootstrap.OtpInput.getOrCreateInstance(el);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
import { QRCodeJs } from '@qr-platform/qr-code.js';
|
||||
|
||||
function initQrCode(el) {
|
||||
const { qrData,qrShape, qrImage, qrIsResponsive} = el.dataset;
|
||||
|
||||
if (!qrData) return;
|
||||
|
||||
const options = { data: qrData };
|
||||
if (qrShape) options.shape = qrShape;
|
||||
if (qrImage) options.image = qrImage;
|
||||
|
||||
options.dotsOptions = { type: 'rounded'}
|
||||
|
||||
if (qrIsResponsive !== undefined) {
|
||||
options.isResponsive = qrIsResponsive === 'true';
|
||||
}
|
||||
|
||||
const qrCode = new QRCodeJs(options);
|
||||
qrCode.append(el, { clearContainer: true });
|
||||
}
|
||||
|
||||
export { initQrCode };
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
Alpine.data('turnstileLogin', () => ({
|
||||
init() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.remove();
|
||||
},
|
||||
|
||||
render() {
|
||||
const widget = this.$refs.widget;
|
||||
if (!widget || !window.turnstile || widget.hasChildNodes()) return;
|
||||
|
||||
window.turnstile.render(widget, {
|
||||
sitekey: widget.dataset.sitekey,
|
||||
size: widget.dataset.size || 'normal',
|
||||
theme: widget.dataset.theme || 'auto',
|
||||
callback: window[widget.dataset.callback] || undefined,
|
||||
'error-callback': window[widget.dataset.errorCallback] || undefined,
|
||||
});
|
||||
},
|
||||
|
||||
remove() {
|
||||
const widget = this.$refs.widget;
|
||||
if (!widget || !window.turnstile) return;
|
||||
|
||||
try {
|
||||
window.turnstile.remove(widget);
|
||||
} catch {
|
||||
// The widget may already have been removed by a prior swap.
|
||||
}
|
||||
},
|
||||
|
||||
removeIfSwapped(event) {
|
||||
const target = event.detail.target;
|
||||
if (target === this.$el || target?.contains(this.$refs.widget)) {
|
||||
this.remove();
|
||||
}
|
||||
},
|
||||
|
||||
renderIfSwapped(event) {
|
||||
const target = event.detail.target;
|
||||
if (target === this.$el || target?.contains(this.$el)) {
|
||||
this.$nextTick(() => this.render());
|
||||
}
|
||||
},
|
||||
|
||||
resetIfFailed(event) {
|
||||
if (event.detail.xhr?.status < 400) return;
|
||||
|
||||
const widget = this.$refs.widget;
|
||||
if (widget?.hasChildNodes() && window.turnstile) {
|
||||
window.turnstile.reset(widget);
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
function rootSelector(evt, selector, callback) {
|
||||
const root = evt.detail?.target || evt.detail?.elt || document;
|
||||
if (!root.querySelectorAll) return;
|
||||
root.querySelectorAll(selector + ':not([data-initialized])').forEach(el => {
|
||||
try {
|
||||
callback(el);
|
||||
el.dataset.initialized = 'true';
|
||||
} catch {
|
||||
console.warn('Failed to init element', el);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { rootSelector }
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
import '../styles/styles.scss'
|
||||
|
||||
|
||||
import htmx from 'htmx.org';
|
||||
|
||||
import { HotkeyManager } from '@tanstack/hotkeys'
|
||||
|
||||
import { initQrCode } from './qrcode.js'
|
||||
|
||||
// cheet('s d o', () => {
|
||||
// console.log('sdo!');
|
||||
//})
|
||||
|
||||
HotkeyManager.getInstance().register(['s', 'd', 'o'], () => {
|
||||
console.log('sdo!')
|
||||
});
|
||||
|
||||
window.initQrCode = initQrCode;
|
||||
window.htmx = htmx;
|
||||
@@ -0,0 +1,32 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title th:remove="all">Template for HTML OTP</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>
|
||||
Aquí está su código de un uso
|
||||
</p>
|
||||
|
||||
<h1 th:text="${otp}">:</h1>
|
||||
|
||||
<p>
|
||||
Mensaje generado automáticamente, favor de no contestar.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
login.title=Start your passwordless journey.
|
||||
login.description=Your email address has to be authorized by your administrator to access this service.
|
||||
login.description2=We will guide you through the process of installing Double Octopus authenticator and test it to start your journey.
|
||||
login.email=Corporate email address
|
||||
login.start=Start
|
||||
login.terms=By clicking Sign up, you agree the terms of use from your organization.
|
||||
login.error=Your account is not authorized. Please contact your administrator.
|
||||
|
||||
error.404.page-title=Page not found
|
||||
error.404.title=We couldn't find that page.
|
||||
error.404.description=The address you entered does not lead to a page in this service.
|
||||
error.404.description2=Return to sign in to continue your passwordless journey.
|
||||
error.404.label=Page not found
|
||||
error.404.action=Go to sign in
|
||||
|
||||
login.monitor.title=Follow you users passwordless journey.
|
||||
login.monitor.description=
|
||||
login.monitor.description2=
|
||||
login.monitor.email=Administrator email
|
||||
login.monitor.start=Start
|
||||
login.monitor.terms=
|
||||
login.monitor.error=Invalid account
|
||||
|
||||
|
||||
download.title=Download Octopus Authenticator
|
||||
download.description=You will need to install the Octopus Authenticator app from the App Store or Google Play into your mobile device.
|
||||
download.description2=Your mobile device will be your main authentication device.
|
||||
download.ready=I have downloaded the Octopus Authenticator App.
|
||||
download.qr=Scan QR or open the following URL to download Octopus Authenticator in your mobile device
|
||||
download.requirement=Requirement
|
||||
|
||||
otp.title=Verify your email
|
||||
otp.description=You will need access to your corporate email.
|
||||
otp.description2=We are sending a one time password, please type it in the field to the right.
|
||||
otp.expired=One time password expired!
|
||||
otp.restart=Restart
|
||||
otp.locked=Too many incorrect codes. Please request a new code and try again.
|
||||
otp.resend=Send a new code.
|
||||
|
||||
otpAccepted.title=Enrol your device
|
||||
otpAccepted.description=Scan the QR with Octopus Authenticator
|
||||
otpAccepted.description2=
|
||||
|
||||
push.title=Test it!
|
||||
push.description=You will receive a push notification to test your enrolled device.
|
||||
push.description2=This way you will be authenticated to your corporate services.
|
||||
push.button=Test it!
|
||||
|
||||
done.title=It's alive
|
||||
done.description=Your notifications are working.
|
||||
done.description2=From now on you can forget dealing with passwords, just authorize with your mobile.
|
||||
done.welcome=Welcome!
|
||||
@@ -0,0 +1,51 @@
|
||||
login.title=Inicie el camino a una operación sin contraseñas.
|
||||
login.description=Su cuenta deberá de estar autorizada por su empresa para el uso de este servicio.
|
||||
login.description2=Le guiaremos en la instalación y configuración del Double Octopus Authenticator.
|
||||
login.email=Correo corporativo
|
||||
login.start=Iniciar
|
||||
login.terms=Al indicar 'Iniciar', acepta los Términos y Condiciones de uso
|
||||
login.error=Su cuenta no está autorizada. Favor de contactar a su administrador.
|
||||
|
||||
error.404.page-title=Página no encontrada
|
||||
error.404.title=No pudimos encontrar esa página.
|
||||
error.404.description=La dirección ingresada no corresponde a una página de este servicio.
|
||||
error.404.description2=Vuelva a iniciar sesión para continuar su camino sin contraseñas.
|
||||
error.404.label=Página no encontrada
|
||||
error.404.action=Ir a iniciar sesión
|
||||
|
||||
login.monitor.title=Siga el camino de sus usuarios.
|
||||
login.monitor.description=
|
||||
login.monitor.description2=
|
||||
login.monitor.email=Correo del administrador
|
||||
login.monitor.start=Iniciar
|
||||
login.monitor.terms=
|
||||
login.monitor.error=Cuenta inválida
|
||||
|
||||
download.title=Instale Octopus Authenticator
|
||||
download.description=Deberá de instalar Octopus Authenticator descargándolo del App Store o Google Play desde su teléfono.
|
||||
download.description2=Su teléfono será su autenticador principal.
|
||||
download.ready=Ya descargué el App Octopus Authenticator
|
||||
download.qr=Escanee el código QR o abra el siguiente URL para descargar el Octopus Authenticator en su teléfono móvil.
|
||||
download.requirement=Requerimiento
|
||||
|
||||
push.title=¡Probemos!
|
||||
push.description=Le vamos a enviar una notificación para que pruebe su dispositivo recientemente enrolado.
|
||||
push.description2=En adelante esta será la forma de ingresar a sus servicios corporativos.
|
||||
push.button=Probar ahora
|
||||
|
||||
done.title=¡Funciona!
|
||||
done.description=Las notificaciones funcionan.
|
||||
done.description2=A partir de este momento se puede olvidar de gestionar su contraseña, sólo debe de autorizar el acceso con su móvil.
|
||||
done.welcome=¡Bienvenido!
|
||||
|
||||
otp.title=Verifique su correo
|
||||
otp.description=Va a necesitar acceso a su correo corporativo.
|
||||
otp.description2=Le vamos a enviar un código a su correo, favor de ingresarlo a continuación.
|
||||
otp.expired=Expiró el código
|
||||
otp.restart=Reenviar
|
||||
otp.locked=Demasiados códigos incorrectos. Solicite un código nuevo e intente de nuevo.
|
||||
otp.resend=Enviar un código nuevo
|
||||
|
||||
otpAccepted.title=Inscriba su dispositivo
|
||||
otpAccepted.description=Abra la aplicación Octopus Authenticator y
|
||||
otpAccepted.description2=escanee el QR para terminar su inscripción
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 699 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,46 @@
|
||||
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
|
||||
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
|
||||
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
|
||||
</g>
|
||||
<g id="_Group_" data-name="<Group>">
|
||||
<g id="_Group_2" data-name="<Group>">
|
||||
<g id="_Group_3" data-name="<Group>">
|
||||
<path id="_Path_" data-name="<Path>" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
|
||||
<path id="_Path_2" data-name="<Path>" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
|
||||
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
|
||||
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
|
||||
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
|
||||
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_Group_4" data-name="<Group>">
|
||||
<g>
|
||||
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
|
||||
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
|
||||
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
|
||||
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
|
||||
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 175.6 223" width="175.6" height="223">
|
||||
<path fill="#272D58" d="m87.7 0-54.9 31.7v63.4l54.9 31.7 54.9-31.7v-63.4zm34.5 83.3-34.5 19.9-34.5-19.9v-39.8l34.5-19.9 34.5 19.9z"/>
|
||||
<path fill="#272D58" d="m133.3 142.4c-5.1-2.7-10.1-5.8-14.9-9.1-13.8-10.2-16.5-33.9-16.6-34.1l-5.2 0.6-3.2 0.2-5.6 3.2-5.4-3.1-5.7-0.3-5.2-0.5c0 0.2-2.9 24-16.6 34.1-4.8 3.3-9.8 6.3-14.9 9.1-17.8 10-40 22.4-40 46.2v33.8h17.1v-33.8c0-13.8 15.9-22.6 31.2-31.2l0.1-0.1c-6.3 9.2-9.6 20.2-9.4 31.3v33.8h17.2v-33.8c0-15.5 7.6-24 15.6-33 2.1-2.4 4.3-4.8 6.3-7.4v74.2h17.1v-74.3c2 2.6 4.2 5 6.3 7.4 8 9 15.6 17.5 15.6 33v33.8h17.1v-33.7c0.1-11.2-3.1-22.1-9.4-31.3l0.1 0.1c15.4 8.6 31.3 17.5 31.3 31.2v33.8h17.1v-33.8c0-23.9-22.2-36.3-40-46.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 814 B |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 175.6 223" width="175.6" height="223">
|
||||
<rect width="175.6" height="223" fill="#07172F"/>
|
||||
<path fill="#1B2E5C" d="m87.7 0-54.9 31.7v63.4l54.9 31.7 54.9-31.7v-63.4zm34.5 83.3-34.5 19.9-34.5-19.9v-39.8l34.5-19.9 34.5 19.9z"/>
|
||||
<path fill="#1B2E5C" d="m133.3 142.4c-5.1-2.7-10.1-5.8-14.9-9.1-13.8-10.2-16.5-33.9-16.6-34.1l-5.2 0.6-3.2 0.2-5.6 3.2-5.4-3.1-5.7-0.3-5.2-0.5c0 0.2-2.9 24-16.6 34.1-4.8 3.3-9.8 6.3-14.9 9.1-17.8 10-40 22.4-40 46.2v33.8h17.1v-33.8c0-13.8 15.9-22.6 31.2-31.2l0.1-0.1c-6.3 9.2-9.6 20.2-9.4 31.3v33.8h17.2v-33.8c0-15.5 7.6-24 15.6-33 2.1-2.4 4.3-4.8 6.3-7.4v74.2h17.1v-74.3c2 2.6 4.2 5 6.3 7.4 8 9 15.6 17.5 15.6 33v33.8h17.1v-33.7c0.1-11.2-3.1-22.1-9.4-31.3l0.1 0.1c15.4 8.6 31.3 17.5 31.3 31.2v33.8h17.1v-33.8c0-23.9-22.2-36.3-40-46.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 866 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" width="493.2" height="223" version="1.1" viewBox="0 0 493.20001 223.00001" xml:space="preserve"><metadata></metadata><style type="text/css"> .st0{fill:#408CFF;} </style><title>Logo_SDO_D_Converted_Transparent</title><path d="m235.4 59.3c0 5.1-1.2 9.1-3.6 12s-6.4 4.4-11.9 4.4c-5.2 0.1-10.4-1-15.2-3l1.5-5c4.3 1.5 8.8 2.3 13.3 2.3 3.2 0 5.5-0.8 7-2.4s2.2-4 2.2-7.2c0.1-2.5-0.3-5-1.2-7.4-0.8-1.7-2.5-2.7-5.1-3.1l-8-1.1c-3.8-0.6-6.3-2.1-7.6-4.6s-1.9-6.2-1.9-11.3c0-5 1.3-8.9 3.9-11.7s6.2-4.2 10.9-4.2 9.5 0.9 13.9 2.6l-1.5 4.9c-3.6-1.1-7.3-1.6-11-1.7-3.3 0-5.7 0.7-7.2 2s-2.3 3.8-2.3 7.4 0.4 6.2 1.2 7.8 2.4 2.5 4.7 2.8l7.6 1.1c3 0.3 5.8 1.8 7.7 4.3 1.7 2.3 2.6 6 2.6 11.1z"></path><path d="m246.7 74.7v-56.7h27l-0.8 5.7h-19.2v19.8h17.6v5.7h-17.6v19.9h20.8v5.7z"></path><path d="m290 19.1c1.5-0.6 3.1-1 4.7-1.4 2.5-0.5 5-0.7 7.6-0.6 3.2 0 6.4 0.5 9.5 1.5l-1.2 5.3c-2.6-0.6-5.3-1-8-1-2 0-3.9 0.1-5.9 0.3-1.1 0.1-2.2 0.5-3.1 1.1-0.7 0.4-1.3 1-1.9 1.5-0.5 0.7-0.9 1.4-1 2.3-0.3 1.9-0.4 3.7-0.3 5.6v25.5c0 4.3 0.4 6.9 1.3 7.9 1.2 1.3 2.8 2.2 4.5 2.6 2.1 0.3 4.1 0.4 6.2 0.4 3-0.1 6-0.5 8.8-1.3l1.5 4.8c-3.7 1.5-7.6 2.3-11.6 2.3-3.9 0-6.6-0.3-8.1-0.8s-2.7-1-3.6-1.4-1.7-1.1-2.3-1.9c-0.7-0.9-1.2-1.7-1.7-2.4-0.6-1-0.9-2-1.1-3.2-0.3-1.5-0.5-2.7-0.7-3.7-0.2-1.5-0.3-3.1-0.3-4.7-0.1-4.1-0.2-8-0.2-11.6s0-6.3 0.1-8.2c0.1-1.8 0.2-4 0.4-6.6 0-1.9 0.4-3.8 1-5.6 0.5-1.1 1.1-2.4 1.9-3.8 1-1.4 2.1-2.4 3.5-2.9z"></path><path d="m335.2 53h-5.3v21.7h-7v-56.7h12.3c6.5 0 11.1 1.3 13.9 3.9s4.1 7.1 4.1 13.6c0 4.4-0.6 7.9-1.9 10.5-1.2 2.5-3.4 4.5-6 5.5l9.3 23.2h-7.1l-8.9-21.8c-0.8 0.1-1.9 0.1-3.4 0.1zm11-15.7v-3.6c0-1.5 0-2.6-0.1-3.4-0.1-0.9-0.3-1.8-0.6-2.6-0.3-0.9-0.9-1.7-1.7-2.3-1.9-1.1-4.7-1.7-8.5-1.7h-5.3v23.5h5.3c3.8 0 6.6-0.6 8.5-1.7 0.8-0.5 1.4-1.3 1.7-2.3 0.3-0.8 0.5-1.7 0.6-2.6 0.1-0.6 0.1-1.8 0.1-3.3z"></path><path d="m366.3 74.7v-56.7h27l-0.8 5.7h-19.2v19.8h17.6v5.7h-17.6v19.9h20.8v5.7z"></path><path d="m397.7 23.7v-5.7h30.8v5.7h-11.9v51h-7v-51z"></path><g fill="#408cff"><path class="st0" d="m207 91.7h16.2c3.9-0.1 7.7 0.5 11.3 1.8 1.4 0.5 2.5 1.5 3.2 2.7s1.4 2.4 1.9 3.6c0.6 1.7 1 3.5 1 5.3 0.3 4 0.5 8.8 0.5 14.5 0 5.6-0.1 9.6-0.2 11.9-0.1 2.6-0.5 5.1-1 7.6-0.6 2.8-1.5 4.7-2.8 5.6-3.2 2.4-7.9 3.6-14 3.6h-16.2zm16.3 5.8h-9.3v44.9h9.3c3.7 0 6.6-0.6 8.5-1.9 1.3-0.9 2.1-2.6 2.3-5.1 0.1-0.8 0.1-2 0.1-3.6v-23.6c0-1.6 0-2.8-0.1-3.6-0.1-1-0.3-1.9-0.6-2.8-0.3-1-0.8-1.8-1.7-2.4-1.9-1.3-4.8-1.9-8.5-1.9z"></path><path class="st0" d="m282.6 147.4c-1.6 0.6-3.2 1-4.8 1.3-2 0.4-4 0.6-6 0.6-2.2 0-4-0.1-5.4-0.2-1.3-0.1-2.6-0.4-3.9-0.8-1.1-0.4-2.1-0.8-3-1.2s-1.7-1.1-2.3-1.9c-0.7-0.9-1.2-1.7-1.7-2.4-0.6-1-0.9-2-1.1-3.2-0.3-1.5-0.5-2.7-0.7-3.7-0.2-1.5-0.3-3.1-0.3-4.7-0.1-4.1-0.2-8-0.2-11.6s0-6.3 0.1-8.2c0.1-1.8 0.2-4 0.4-6.6 0-1.9 0.4-3.8 1-5.5 0.5-1.1 1.1-2.4 1.9-3.8 0.7-1.3 1.8-2.4 3.2-3 3.5-1.3 7.2-2 10.9-1.9 3.1-0.1 6.2 0.3 9.1 1.2 1.9 0.6 3.7 1.6 5.3 2.8 1.3 1.1 2.2 3.1 2.8 6 0.5 2.6 0.9 5.3 1 7.9 0.1 2.4 0.2 5.8 0.2 10.4s0 7.8-0.1 9.6-0.2 4-0.4 6.6c0 1.9-0.4 3.8-1 5.5-0.6 1.3-1.2 2.5-1.9 3.8-0.6 1.4-1.7 2.5-3.1 3zm-0.4-14.7v-25.4c0-4.3-0.5-6.9-1.4-7.9-1.1-1.3-2.7-2.2-4.4-2.6-1.7-0.3-3.4-0.4-5.1-0.4-1.5 0-3.1 0.1-4.6 0.3-1.1 0.1-2.2 0.5-3.1 1.1-0.7 0.4-1.3 1-1.9 1.5-0.5 0.7-0.9 1.4-1 2.3-0.3 1.9-0.4 3.7-0.3 5.6v25.4c0 4.3 0.4 6.9 1.3 7.9 1.2 1.3 2.7 2.3 4.5 2.6 1.7 0.3 3.4 0.4 5.1 0.4 1.5 0 3.1-0.1 4.6-0.3 1.1-0.1 2.1-0.5 3.1-1.1 0.7-0.4 1.3-1 1.9-1.5 0.5-0.6 0.9-1.4 0.9-2.3 0.3-1.8 0.4-3.7 0.4-5.6z"></path><path class="st0" d="m330.9 132.7v-41.1h7v28.4c0 11.6-0.5 18.4-1.5 20.6-0.5 1.3-1.2 2.6-1.9 3.8-0.7 1.3-1.8 2.3-3.2 2.9-1.5 0.6-3.1 1-4.7 1.3-2.2 0.4-4.4 0.6-6.6 0.6s-4.4-0.2-6.6-0.6c-1.6-0.3-3.2-0.8-4.7-1.3-1.4-0.6-2.5-1.6-3.2-2.9-0.7-1.2-1.3-2.5-1.9-3.8-0.6-1.8-0.9-3.6-1-5.5-0.3-3.2-0.4-8.3-0.4-15.1v-28.4h7v41c0 1.6 0 2.8 0.1 3.6 0.1 1 0.3 1.9 0.6 2.8 0.3 1 0.8 1.8 1.7 2.4 1 0.6 2.1 1 3.3 1.3 1.7 0.4 3.5 0.6 5.3 0.6s3.5-0.2 5.3-0.6c1.1-0.3 2.2-0.7 3.2-1.3 0.8-0.6 1.4-1.4 1.7-2.4 0.3-0.9 0.5-1.8 0.6-2.8-0.1-0.7-0.1-1.9-0.1-3.5z"></path><path class="st0" d="m352.1 91.7h13.4c5.9 0 10 1.2 12.4 3.7s3.6 6.7 3.6 12.8c0 6-1.9 10-5.7 11.8 2.3 0.7 4.3 2.3 5.5 4.3s1.7 4.8 1.7 8.3c0 5.6-1.3 9.7-3.9 12.1s-7 3.7-13.3 3.7h-13.8v-56.7zm22.3 17.5v-3.2c0-3.9-0.6-6.3-1.9-7.1-1.5-1-3.8-1.5-7-1.5h-6.4v20.4h5.6c3.5 0 6.1-0.5 7.7-1.5 1.2-0.7 1.9-2.1 2-4.2v-2.9zm1.6 24.9v-3.2c0-3.3-0.4-5.4-1.3-6.1-1.3-1.1-2.9-1.8-4.5-2.1-1.7-0.2-3.3-0.3-5-0.3h-6.1v20.2h7c3.6 0 6.3-0.5 7.9-1.5 1.2-0.7 1.9-2 2-4v-3z"></path><path class="st0" d="m396.4 148.3v-56.7h7v51h18.3v5.7z"></path><path class="st0" d="m430.6 148.3v-56.7h27l-0.8 5.7h-19.2v19.8h17.6v5.7h-17.6v19.9h20.8v5.7z"></path><path class="st0" d="m235.1 221c-1.6 0.6-3.2 1-4.8 1.3-2 0.4-4 0.6-6 0.6-2.2 0-4-0.1-5.4-0.2-1.3-0.1-2.6-0.4-3.9-0.8-1.1-0.4-2.1-0.8-3-1.2s-1.7-1.1-2.3-1.9c-0.7-0.9-1.2-1.7-1.7-2.4-0.6-1-0.9-2-1.1-3.Line truncated
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 175.6 223" width="175.6" height="223">
|
||||
<path fill="#B8C5FB" d="m87.7 0-54.9 31.7v63.4l54.9 31.7 54.9-31.7v-63.4zm34.5 83.3-34.5 19.9-34.5-19.9v-39.8l34.5-19.9 34.5 19.9z"/>
|
||||
<path fill="#B8C5FB" d="m133.3 142.4c-5.1-2.7-10.1-5.8-14.9-9.1-13.8-10.2-16.5-33.9-16.6-34.1l-5.2 0.6-3.2 0.2-5.6 3.2-5.4-3.1-5.7-0.3-5.2-0.5c0 0.2-2.9 24-16.6 34.1-4.8 3.3-9.8 6.3-14.9 9.1-17.8 10-40 22.4-40 46.2v33.8h17.1v-33.8c0-13.8 15.9-22.6 31.2-31.2l0.1-0.1c-6.3 9.2-9.6 20.2-9.4 31.3v33.8h17.2v-33.8c0-15.5 7.6-24 15.6-33 2.1-2.4 4.3-4.8 6.3-7.4v74.2h17.1v-74.3c2 2.6 4.2 5 6.3 7.4 8 9 15.6 17.5 15.6 33v33.8h17.1v-33.7c0.1-11.2-3.1-22.1-9.4-31.3l0.1 0.1c15.4 8.6 31.3 17.5 31.3 31.2v33.8h17.1v-33.8c0-23.9-22.2-36.3-40-46.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 814 B |
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,18 @@
|
||||
ou{
|
||||
"name": "redINTER",
|
||||
"short_name": "redINTER",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon/favicon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/favicon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
@use "datatables.net-dt";
|
||||
@@ -0,0 +1,130 @@
|
||||
/*!
|
||||
* Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
*
|
||||
* This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
* proprietary and confidential software.
|
||||
*
|
||||
* Reproduction, modification, distribution, disclosure, or use of this
|
||||
* source code without the copyright holder's prior written authorization
|
||||
* is prohibited, except as permitted by applicable law or a valid
|
||||
* license agreement.
|
||||
*/
|
||||
|
||||
|
||||
@use 'bootstrap/scss/bootstrap';
|
||||
|
||||
:root {
|
||||
--bs-body-font-family: "Geist", system-ui, sans-serif;
|
||||
--bs-font-mono: "Geist Mono", ui-monospace, monospace;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.layout-content > .container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.customer-logo-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 7.5rem;
|
||||
max-width: 32rem;
|
||||
margin: 0 auto 0.75rem;
|
||||
padding: 1.5rem 2rem;
|
||||
background: white;
|
||||
border: 1px solid rgba(black, 0.08);
|
||||
border-radius: var(--bs-border-radius-lg);
|
||||
box-shadow: 0 0.5rem 1.5rem rgba(black, 0.12);
|
||||
}
|
||||
|
||||
.customer-logo-header + section > .container {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.customer-logo {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 6rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.store-downloads {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 0.75rem;
|
||||
}
|
||||
|
||||
.store-downloads td {
|
||||
padding: 0.25rem 0.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.store-downloads img {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-width: 8rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.enrollment-carousel-image-frame {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 4 / 3;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
border-radius: var(--bs-border-radius-lg);
|
||||
}
|
||||
|
||||
.enrollment-carousel-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.otp {
|
||||
--otp-size: 3.5rem;
|
||||
--otp-font-size: 1.25rem;
|
||||
--otp-slot-border-width: 2px;
|
||||
}
|
||||
|
||||
#otp-pane {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#flash {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.75rem);
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.customer-logo-panel {
|
||||
min-height: 6rem;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
.customer-logo {
|
||||
max-height: 4.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout.html(entrypoint='done')}"
|
||||
xmlns:hx-on="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<section class="layout-content" layout:fragment="content">
|
||||
<div class="container xl:col-10 2xl:col-8 px-4 py-5">
|
||||
<div class="row align-items-center lg:g-5 py-5">
|
||||
<div layout:insert="~{fragments/panel::left(#{done.title},#{done.description},#{done.description2})}"
|
||||
class="lg:col-7 text-center lg:text-start">
|
||||
</div>
|
||||
<div class="md:col-10 mx-auto lg:col-5">
|
||||
<h1 th:text="#{done.welcome}">Welcome</h1>
|
||||
<img src="/img/sdo-logo.svg" alt="Secret Double Octopus"
|
||||
hx-on:load="window.party.confetti(this)"
|
||||
onmousedown="window.party.confetti(this)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout.html(entrypoint='download')}" xmlns:hx-on="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>Download</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="layout-content" layout:fragment="content">
|
||||
<div class="container xl:col-10 2xl:col-8 px-4 py-5">
|
||||
<div class="row align-items-center lg:g-5 py-5">
|
||||
<div layout:insert="~{fragments/panel::left(#{download.title},#{download.description},#{download.description2})}"
|
||||
class="lg:col-7 text-center lg:text-start">
|
||||
<div layout:fragment="left-content">
|
||||
<a th:href="@{/(step=${nextStep})}"
|
||||
type="button" class="theme-primary btn-solid btn-styled btn-lg">
|
||||
<img th:src="@{/img/sdo.svg}" class="bi me-1" width="16" height="16" alt="SDO">
|
||||
<span th:text="#{download.ready}"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-10 mx-auto lg:col-5">
|
||||
<div class="container d-flex flex-column align-items-center text-center">
|
||||
<table class="store-downloads mb-3" aria-label="Octopus Authenticator downloads">
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td th:text="#{download.requirement}">Requirement</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="app: ${apps}">
|
||||
<td>
|
||||
<a th:href="${app.link}" th:aria-label="|Download for ${app.os}|" target="_target">
|
||||
<img th:src="@{${app.logo}}" th:alt="|Download for ${app.os}|">
|
||||
</a>
|
||||
</td>
|
||||
<td th:text="${app.version}">8</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mb-3 m-3" th:unless="${shortLink == null}">
|
||||
<figure class="figure" style="width: 240px;">
|
||||
<div
|
||||
id="qr"
|
||||
aria-label="QR code to download the SDO Authenticator"
|
||||
class="d-block mx-auto"
|
||||
|
||||
th:data-qr-data="${shortLink}"
|
||||
data-qr-shape="circle"
|
||||
th:data-qr-image="@{/img/sdo-blue.svg}"
|
||||
data-qr-is-responsive="true"
|
||||
hx-on:load="initQrCode(this)">
|
||||
</div>
|
||||
<figcaption class="figure-caption text-wrap" th:text="#{download.qr}">Scan QR to download the SDO Authenticator in your mobile device</figcaption>
|
||||
</figure>
|
||||
<h3 th:text="${shortLink}"></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
@@ -0,0 +1,85 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:hx="http://www.w3.org/1999/xhtml"
|
||||
xmlns:hx-on="http://www.w3.org/1999/xhtml"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout.html(entrypoint='enroll')}"
|
||||
>
|
||||
<head>
|
||||
<title>Enroll</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="layout-content" layout:fragment="content">
|
||||
<div class="container xl:col-10 2xl:col-8 px-4 py-5">
|
||||
<div class="row align-items-center lg:g-5 py-5">
|
||||
<div layout:insert="~{fragments/panel::left(#{otpAccepted.title},#{otpAccepted.description},#{otpAccepted.description2})}"
|
||||
class="lg:col-7 text-center lg:text-start">
|
||||
</div>
|
||||
<div class="md:col-10 mx-auto lg:col-5">
|
||||
<div class="container d-flex flex-column align-items-center text-center">
|
||||
<div class="md:col-10 mx-auto lg:col-5 position-relative"
|
||||
hx:get="@{/enroll/invite}"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="load">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
|
||||
<section th:fragment="invite">
|
||||
<div aria-label="Invitation to enroll to Octopus Authenticator"
|
||||
class="width: 240px; d-block mx-auto"
|
||||
th:data-qr-data="${invitationLink}"
|
||||
th:data-qr-image="@{/img/sdo-blue.svg}"
|
||||
hx-on:load="window.initQrCode(this)"></div>
|
||||
<!--
|
||||
<div class="tick d-flex justify-content-center"
|
||||
th:data-otp-expiry="180"
|
||||
data-credits="false">
|
||||
<div data-repeat="true"
|
||||
data-layout="horizontal"
|
||||
data-transform="preset(m,s) -> delay">
|
||||
<div class="tick-group">
|
||||
<div data-key="value" data-repeat="true" data-transform="pad(00) -> split -> delay">
|
||||
<span data-view="flip"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div th:unless="${demo != null}"
|
||||
hx:get="@{/enroll/status}"
|
||||
hx-trigger="every 3s [document.visibilityState === 'visible']">
|
||||
</div>
|
||||
<div th:if="${demo != null}"
|
||||
hx:get="@{/enroll/status}"
|
||||
hx-trigger="demo from:body">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section th:fragment="waiting">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout.html}">
|
||||
<head>
|
||||
<title th:text="#{error.404.page-title}">Page not found</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="layout-content" layout:fragment="content">
|
||||
<div class="container xl:col-10 2xl:col-8 px-4 py-5">
|
||||
<div class="row align-items-center lg:g-5 py-5">
|
||||
<div layout:insert="~{fragments/panel::left(#{error.404.title},#{error.404.description},#{error.404.description2})}"
|
||||
class="lg:col-7 text-center lg:text-start"></div>
|
||||
|
||||
<div class="md:col-10 mx-auto lg:col-5">
|
||||
<div class="p-4 p-md-5 border rounded-3 bg-body-tertiary bg-opacity-75 text-center">
|
||||
<p class="display-1 fw-bold text-primary mb-3" aria-hidden="true">404</p>
|
||||
<p class="fs-5 mb-4" th:text="#{error.404.label}">Page not found</p>
|
||||
<a class="w-100 btn btn-lg btn-primary" th:href="@{/login}" th:text="#{error.404.action}">
|
||||
Go to sign in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
>
|
||||
|
||||
<body>
|
||||
|
||||
<div layout:fragment="left(title, d1, d2)">
|
||||
<h1 class="fs-5xl fw-bold lh-1 fg-emphasis-inverse mb-3" th:text="${title}">Title</h1>
|
||||
<p class="lg:col-10 fs-xl" th:text="${d1}">Description 1</p>
|
||||
<p class="lg:col-10 fs-xl" th:text="${d2}">Description 2</p>
|
||||
<div layout:fragment="left-content">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
xmlns:webpack="http://redinterglobal.com/thymeleaf/webpack">
|
||||
<head>
|
||||
<link webpack:entrypoint="${entrypoint}" th:unless="${entrypoint} == null">
|
||||
<link webpack:entrypoint="welcome" th:if="${entrypoint} == null">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">Passwordfree</title>
|
||||
<link rel="icon" type="image/x-icon" th:href="@{/favicon/favicon.ico}">
|
||||
<link rel="icon" type="image/png" sizes="16x16" th:href="@{/favicon/favicon/favicon-16x16.png}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" th:href="@{/favicon/favicon-32x32.png}">
|
||||
<link rel="apple-touch-icon" sizes="180x180" th:href="@{/favicon/apple-touch-icon.png}">
|
||||
<link rel="manifest" th:href="@{/site.webmanifest}">
|
||||
<style>
|
||||
body {
|
||||
background: url("[(@{/img/sdo.svg})]");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html body {
|
||||
background-color: #07172F;
|
||||
background-image: url("[(@{/img/sdo-dark.svg})]");
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="customer-logo-header" sec:authorize="isAuthenticated()">
|
||||
<div class="container col-xl-10 col-xxl-8 px-4 pt-5">
|
||||
<div class="customer-logo-panel">
|
||||
<img th:src="@{/logo}" class="customer-logo" alt="Customer logo" sec:authorize="hasRole('ROLE_USER')">
|
||||
<img th:src="@{/monitor/logo}" class="customer-logo" alt="Customer logo" sec:authorize="hasRole('ROLE_MONITOR')">
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="layout-content" layout:fragment="content">
|
||||
</section>
|
||||
|
||||
</body>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout.html(entrypoint='login-page')}"
|
||||
>
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<th:block th:if="${turnstileEnabled}">
|
||||
<link rel="preconnect" href="https://challenges.cloudflare.com"/>
|
||||
<script>
|
||||
window.turnstileHtmxOnLoad = function () {
|
||||
window.dispatchEvent(new Event('turnstile:ready'));
|
||||
};
|
||||
</script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=turnstileHtmxOnLoad" async defer></script>
|
||||
</th:block>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="layout-content" layout:fragment="content">
|
||||
<div class="container xl:col-10 2xl:col-8 px-4 py-5">
|
||||
<div class="row align-items-center lg:g-5 py-5">
|
||||
|
||||
<div layout:insert="~{fragments/panel::left(#{login.title},#{login.description},#{login.description2})}"
|
||||
class="lg:col-7 text-center lg:text-start"></div>
|
||||
|
||||
<div class="md:col-10 mx-auto lg:col-5 position-relative">
|
||||
<form id="login-form"
|
||||
class="p-4 p-md-5 border rounded-3 bg-body-tertiary bg-opacity-75"
|
||||
hx:post="@{/login}"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#spinner"
|
||||
x-data="turnstileLogin"
|
||||
x-on:turnstile:ready.window="render()"
|
||||
x-on:before-swap.camel="removeIfSwapped($event)"
|
||||
x-on:after-swap.camel="renderIfSwapped($event)"
|
||||
x-on:after-request.camel="resetIfFailed($event)"
|
||||
hx-status:401="swap:none"
|
||||
>
|
||||
<div class="form-floating mb-3">
|
||||
<label for="floatingInput" th:text="#{login.email}">Corporate email address</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="floatingInput"
|
||||
name="email"
|
||||
placeholder="name@example.com"/>
|
||||
</div>
|
||||
<button class="w-100 btn btn-lg btn-primary" type="submit">
|
||||
<span id="spinner" class="htmx-indicator">
|
||||
<div class="spinner-border spinner-border-sm" aria-hidden="true" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</span>
|
||||
<span th:text="#{login.start}"></span>
|
||||
</button>
|
||||
<small class="text-body-secondary" th:text="#{login.terms}">
|
||||
>By clicking Sign up, you agree to the terms of use.</small>
|
||||
<th:block th:if="${turnstileEnabled}">
|
||||
<hr class="my-4" />
|
||||
<div class="form-floating mb-3">
|
||||
<div x-ref="widget" th:data-sitekey="${turnstileSiteKey}" data-size="normal"></div>
|
||||
</div>
|
||||
</th:block>
|
||||
</form>
|
||||
<div id="flash"
|
||||
th:hidden="${loginError != true}"
|
||||
class="alert flash theme-warning d-flex align-items-center"
|
||||
role="alert"
|
||||
hx-on="flash -> clearTimeout(this._flashTimeout);
|
||||
this.textContent = message;
|
||||
this.hidden = !this.textContent;
|
||||
if (!this.hidden) this._flashTimeout = setTimeout(() => { this.textContent = ''; this.hidden = true; }, 3000)"
|
||||
>
|
||||
<span th:if="${loginError==true}" th:text="#{login.error}">Your account is not authorized. Please contact your administrator.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout.html(entrypoint='login-page')}">
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<th:block th:if="${turnstileEnabled}">
|
||||
<link rel="preconnect" href="https://challenges.cloudflare.com"/>
|
||||
<script>
|
||||
window.turnstileHtmxOnLoad = function () {
|
||||
window.dispatchEvent(new Event('turnstile:ready'));
|
||||
};
|
||||
</script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=turnstileHtmxOnLoad" async defer></script>
|
||||
</th:block>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="layout-content" layout:fragment="content">
|
||||
<div class="container xl:col-10 2xl:col-8 px-4 py-5">
|
||||
<div class="row align-items-center lg:g-5 py-5">
|
||||
|
||||
<div layout:insert="~{fragments/panel::left(#{login.monitor.title},#{login.monitor.description},#{login.monitor.description2})}"
|
||||
class="lg:col-7 text-center lg:text-start"></div>
|
||||
|
||||
<div class="md:col-10 mx-auto lg:col-5 position-relative">
|
||||
<form id="login-form"
|
||||
class="p-4 p-md-5 border rounded-3 bg-body-tertiary bg-opacity-75"
|
||||
hx:post="@{/monitor/login/select}"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#spinner"
|
||||
x-data="turnstileLogin"
|
||||
x-on:turnstile:ready.window="render()"
|
||||
x-on:before-swap.camel="removeIfSwapped($event)"
|
||||
x-on:after-swap.camel="renderIfSwapped($event)"
|
||||
x-on:after-request.camel="resetIfFailed($event)"
|
||||
hx-status:401="swap:none"
|
||||
>
|
||||
<div class="form-floating mb-3">
|
||||
<label for="floatingInput" th:text="#{login.monitor.email}">Corporate email address</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="floatingInput"
|
||||
name="email"
|
||||
placeholder="name@example.com"
|
||||
th:value="${monitorEmail}"/>
|
||||
</div>
|
||||
<button class="w-100 btn btn-lg btn-primary" type="submit">
|
||||
<span id="spinner" class="htmx-indicator">
|
||||
<div class="spinner-border spinner-border-sm" aria-hidden="true" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</span>
|
||||
<span th:text="#{login.monitor.start}"></span>
|
||||
</button>
|
||||
<small class="text-body-secondary" th:text="#{login.monitor.terms}">
|
||||
>By clicking Sign up, you agree to the terms of use.</small>
|
||||
<th:block th:if="${turnstileEnabled}">
|
||||
<hr class="my-4" />
|
||||
<div class="form-floating mb-3">
|
||||
<div x-ref="widget" th:data-sitekey="${turnstileSiteKey}" data-size="normal"></div>
|
||||
</div>
|
||||
</th:block>
|
||||
</form>
|
||||
<div id="flash"
|
||||
th:hidden="${loginError != true}"
|
||||
class="alert flash theme-warning d-flex align-items-center"
|
||||
role="alert"
|
||||
hx-on="flash -> clearTimeout(this._flashTimeout);
|
||||
this.textContent = message;
|
||||
this.hidden = !this.textContent;
|
||||
if (!this.hidden) this._flashTimeout = setTimeout(() => { this.textContent = ''; this.hidden = true; }, 3000)"
|
||||
>
|
||||
<span th:if="${loginError==true}" th:text="#{login.monitor.error}">Your account is not authorized. Please contact your administrator.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout.html(entrypoint='monitor')}"
|
||||
xmlns:hx="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>Monitor</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="layout-content" layout:fragment="content">
|
||||
<div class="container xl:col-10 2xl:col-8 px-4 py-5 align-self-start"
|
||||
hx-sse:connect="/monitor/sse"
|
||||
hx-on:enrolled="
|
||||
console.log('enrolled', event.detail.data);
|
||||
htmx.trigger(document.body, 'updated');
|
||||
"
|
||||
>
|
||||
<div id="data">
|
||||
<table id="table" class="table" th:section="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Email</th>
|
||||
<th scope="col">Step</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,115 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout.html(entrypoint='otp')}"
|
||||
xmlns:hx="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<title>OTP Verification</title>
|
||||
</head>
|
||||
|
||||
<section class="layout-content" layout:fragment="content">
|
||||
|
||||
<div id="otp-expired" hx-trigger="otp-expired"
|
||||
hx:get="@{/otpExpired}"
|
||||
hx-target="#otp-pane"
|
||||
hx-swap="outerHTML"></div>
|
||||
|
||||
<div class="container xl:col-10 2xl:col-8 px-4 py-5">
|
||||
<div id="pane-controller" class="row align-items-center lg:g-5 py-5"
|
||||
hx:get="@{/otpAccepted}"
|
||||
hx-trigger="otp-accepted"
|
||||
hx-target="#instructions-pane">
|
||||
|
||||
<div id="instructions-pane" layout:insert="~{fragments/panel::left(#{otp.title},#{otp.description},#{otp.description2})}"
|
||||
class="lg:col-7 text-center lg:text-start">
|
||||
</div>
|
||||
|
||||
<div id="otp-pane" class="md:col-10 mx-auto lg:col-5 position-relative"
|
||||
hx:get="@{/otp}"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="load">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div th:fragment="timer" id="otp-pane" class="md:col-10 mx-auto lg:col-5 position-relative">
|
||||
<div id="timer">
|
||||
<div class="tick d-flex justify-content-center"
|
||||
th:data-otp-expiry="${otpExpirySeconds}"
|
||||
data-credits="false">
|
||||
<div data-repeat="true"
|
||||
data-layout="horizontal"
|
||||
data-transform="preset(m,s) -> delay">
|
||||
<div class="tick-group">
|
||||
<div data-key="value" data-repeat="true" data-transform="pad(00) -> split -> delay">
|
||||
<span data-view="flip"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="otp otp-lg mt-9 justify-content-center" data-bs-otp
|
||||
hx-include="[name='otp']"
|
||||
hx-status:429="target:#otp-pane swap:outerHTML"
|
||||
hx-status:4xx="swap:none"
|
||||
hx-trigger="complete.bs.otpInput"
|
||||
hx:post="@{/otp}"
|
||||
hx-target="#otp-pane"
|
||||
hx-swap="outerHTML"
|
||||
|
||||
>
|
||||
<input name="otp" type="text" class="otp-input" maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
aria-label="Verification code">
|
||||
</div>
|
||||
<div id="flash"
|
||||
hidden
|
||||
class="alert flash theme-warning d-flex align-items-center"
|
||||
role="alert"
|
||||
hx-on="flash -> q('previous .otp').otp.clear();
|
||||
clearTimeout(this._flashTimeout);
|
||||
this.textContent = message;
|
||||
this.hidden = !this.textContent;
|
||||
if (!this.hidden) this._flashTimeout = setTimeout(() => { this.textContent = ''; this.hidden = true; }, 3000)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:fragment="otpLocked" id="otp-pane" class="md:col-10 mx-auto lg:col-5 position-relative text-center">
|
||||
<p class="alert theme-warning" role="alert" th:text="#{otp.locked}">
|
||||
Too many incorrect codes. Please request a new code and try again.
|
||||
</p>
|
||||
<button class="btn btn-primary"
|
||||
hx:post="@{/otp/resend}"
|
||||
hx-target="#otp-pane"
|
||||
hx-swap="outerHTML"
|
||||
th:text="#{otp.resend}">Send a new code</button>
|
||||
</div>
|
||||
|
||||
<div th:fragment="otpExpired" id="otp-pane" class="md:col-10 mx-auto lg:col-5 position-relative text-center">
|
||||
<p class="alert theme-warning" role="alert" th:text="#{otp.expired}">
|
||||
One time password expired!
|
||||
</p>
|
||||
<button class="btn btn-primary"
|
||||
hx:post="@{/otp/resend}"
|
||||
hx-target="#otp-pane"
|
||||
hx-swap="outerHTML"
|
||||
th:text="#{otp.resend}">Send a new code</button>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,52 @@
|
||||
<!--
|
||||
~ Copyright (c) 2026. RedINTER Comunicaciones SA de CV. All rights reserved.
|
||||
~
|
||||
~ This source code is the property of RedINTER Comunicaciones SA de CV and constitutes
|
||||
~ proprietary and confidential software.
|
||||
~
|
||||
~ Reproduction, modification, distribution, disclosure, or use of this
|
||||
~ source code without the copyright holder's prior written authorization
|
||||
~ is prohibited, except as permitted by applicable law or a valid
|
||||
~ license agreement.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout.html}"
|
||||
xmlns:hx="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<section class="layout-content" layout:fragment="content">
|
||||
<div class="container xl:col-10 2xl:col-8 px-4 py-5">
|
||||
<div class="row align-items-center lg:g-5 py-5">
|
||||
|
||||
<div layout:insert="~{fragments/panel::left(#{push.title},#{push.description},#{push.description2})}"
|
||||
class="lg:col-7 text-center lg:text-start">
|
||||
</div>
|
||||
|
||||
<div class="md:col-10 mx-auto lg:col-5">
|
||||
<div class="ratio ratio-16x9 rounded overflow-hidden shadow">
|
||||
<video width="100%" class = "object-fit-contain border rounded" autoplay loop>
|
||||
<source th:src="@{img/push.webm}" type="video/webm">
|
||||
</video>
|
||||
</div>
|
||||
<button type="button" class="w-100 btn btn-lg btn-primary m-9"
|
||||
hx:get="@{/push/now}"
|
||||
hx-indicator="#spinner"
|
||||
hx-trigger="click">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-broadcast-pin" viewBox="0 0 16 16">
|
||||
<path d="M3.05 3.05a7 7 0 0 0 0 9.9.5.5 0 0 1-.707.707 8 8 0 0 1 0-11.314.5.5 0 0 1 .707.707m2.122 2.122a4 4 0 0 0 0 5.656.5.5 0 1 1-.708.708 5 5 0 0 1 0-7.072.5.5 0 0 1 .708.708m5.656-.708a.5.5 0 0 1 .708 0 5 5 0 0 1 0 7.072.5.5 0 1 1-.708-.708 4 4 0 0 0 0-5.656.5.5 0 0 1 0-.708m2.122-2.12a.5.5 0 0 1 .707 0 8 8 0 0 1 0 11.313.5.5 0 0 1-.707-.707 7 7 0 0 0 0-9.9.5.5 0 0 1 0-.707zM6 8a2 2 0 1 1 2.5 1.937V15.5a.5.5 0 0 1-1 0V9.937A2 2 0 0 1 6 8"/>
|
||||
</svg>
|
||||
<div id="spinner" class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true" role="status">
|
||||
<span class="visually-hidden">Waiting...</span>
|
||||
</div>
|
||||
<span th:text="#{push.button}"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div th:fragment="push">
|
||||
Done!
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.redinterglobal.welcome.config
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import org.springframework.mock.web.MockHttpServletRequest
|
||||
import org.springframework.mock.web.MockHttpServletResponse
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.oauth2.core.user.DefaultOAuth2User
|
||||
|
||||
class MonitorAuthenticationSuccessHandlerTest {
|
||||
|
||||
private val handler = MonitorAuthenticationSuccessHandler()
|
||||
|
||||
@Test
|
||||
fun `successful monitor login saves email in a secure cookie`() {
|
||||
val request = MockHttpServletRequest().apply { isSecure = true }
|
||||
val response = MockHttpServletResponse()
|
||||
|
||||
handler.onAuthenticationSuccess(request, response, authentication(" user@example.com "))
|
||||
|
||||
val cookie = requireNotNull(response.getCookie(MonitorAuthenticationSuccessHandler.COOKIE_NAME))
|
||||
assertEquals("user@example.com", cookie.value)
|
||||
assertEquals("/", cookie.path)
|
||||
assertEquals(MonitorAuthenticationSuccessHandler.COOKIE_MAX_AGE_SECONDS, cookie.maxAge)
|
||||
assertEquals(true, cookie.isHttpOnly)
|
||||
assertEquals(true, cookie.secure)
|
||||
assertEquals("Lax", cookie.getAttribute("SameSite"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successful monitor login without an email claim does not save a cookie`() {
|
||||
val request = MockHttpServletRequest()
|
||||
val response = MockHttpServletResponse()
|
||||
val principal = DefaultOAuth2User(
|
||||
listOf(SimpleGrantedAuthority("ROLE_MONITOR")),
|
||||
mapOf("name" to "Monitor User"),
|
||||
"name"
|
||||
)
|
||||
|
||||
handler.onAuthenticationSuccess(request, response, TestingAuthenticationToken(principal, null))
|
||||
|
||||
assertNull(response.getCookie(MonitorAuthenticationSuccessHandler.COOKIE_NAME))
|
||||
}
|
||||
|
||||
private fun authentication(email: String) = TestingAuthenticationToken(
|
||||
DefaultOAuth2User(
|
||||
listOf(SimpleGrantedAuthority("ROLE_MONITOR")),
|
||||
mapOf("email" to email),
|
||||
"email"
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.redinterglobal.welcome.config
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import org.springframework.mock.web.MockHttpServletRequest
|
||||
import org.springframework.mock.web.MockHttpServletResponse
|
||||
import org.springframework.mock.web.MockHttpSession
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
|
||||
class MonitorLoginHintAuthorizationRequestResolverTest {
|
||||
|
||||
private val resolver = MonitorLoginHintAuthorizationRequestResolver(registrationRepository())
|
||||
|
||||
@Test
|
||||
fun `adds login hint from a matching pending session value`() {
|
||||
val session = pendingHintSession()
|
||||
|
||||
val resolved = assertNotNull(resolver.resolve(authorizationRequest(session)))
|
||||
|
||||
assertEquals("user@example.com", resolved.additionalParameters["login_hint"])
|
||||
assertNotNull(resolved.state)
|
||||
assertNull(session.getAttribute(MonitorLoginHintAuthorizationRequestResolver.PENDING_LOGIN_HINT_SESSION_ATTRIBUTE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leaves authorization request unchanged without a pending login hint`() {
|
||||
val resolved = assertNotNull(resolver.resolve(authorizationRequest()))
|
||||
|
||||
assertNull(resolved.additionalParameters["login_hint"])
|
||||
assertNotNull(resolved.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does not consume a hint for another registration`() {
|
||||
val session = MockHttpSession().apply {
|
||||
setAttribute(
|
||||
MonitorLoginHintAuthorizationRequestResolver.PENDING_LOGIN_HINT_SESSION_ATTRIBUTE,
|
||||
PendingMonitorLoginHint("another-idP", "user@example.com")
|
||||
)
|
||||
}
|
||||
|
||||
val resolved = assertNotNull(resolver.resolve(authorizationRequest(session)))
|
||||
|
||||
assertNull(resolved.additionalParameters["login_hint"])
|
||||
assertNotNull(session.getAttribute(MonitorLoginHintAuthorizationRequestResolver.PENDING_LOGIN_HINT_SESSION_ATTRIBUTE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authorization redirect forwards the login hint to the identity provider`() {
|
||||
val session = pendingHintSession()
|
||||
val response = MockHttpServletResponse()
|
||||
|
||||
OAuth2AuthorizationRequestRedirectFilter(resolver).doFilter(authorizationRequest(session), response) { _, _ -> }
|
||||
|
||||
val parameters = UriComponentsBuilder.fromUriString(assertNotNull(response.redirectedUrl))
|
||||
.build()
|
||||
.queryParams
|
||||
assertEquals("user@example.com", parameters.getFirst("login_hint"))
|
||||
assertNotNull(parameters.getFirst("state"))
|
||||
assertNull(session.getAttribute(MonitorLoginHintAuthorizationRequestResolver.PENDING_LOGIN_HINT_SESSION_ATTRIBUTE))
|
||||
}
|
||||
|
||||
private fun authorizationRequest(session: MockHttpSession? = null) =
|
||||
MockHttpServletRequest("GET", "/oauth2/authorization/monitor-idP").apply {
|
||||
servletPath = "/oauth2/authorization/monitor-idP"
|
||||
session?.let(::setSession)
|
||||
}
|
||||
|
||||
private fun pendingHintSession() = MockHttpSession().apply {
|
||||
setAttribute(
|
||||
MonitorLoginHintAuthorizationRequestResolver.PENDING_LOGIN_HINT_SESSION_ATTRIBUTE,
|
||||
PendingMonitorLoginHint("monitor-idP", "user@example.com")
|
||||
)
|
||||
}
|
||||
|
||||
private fun registrationRepository(): ClientRegistrationRepository = InMemoryClientRegistrationRepository(
|
||||
ClientRegistration.withRegistrationId("monitor-idP")
|
||||
.clientId("monitor-client")
|
||||
.clientSecret("secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
|
||||
.authorizationUri("https://idp.example.com/authorize")
|
||||
.tokenUri("https://idp.example.com/token")
|
||||
.jwkSetUri("https://idp.example.com/jwks")
|
||||
.scope(OidcScopes.OPENID)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.redinterglobal.welcome.controller
|
||||
|
||||
import com.digitalsanctuary.cf.turnstile.service.TurnstileValidationService
|
||||
import com.redinterglobal.welcome.config.MonitorLoginHintAuthorizationRequestResolver
|
||||
import com.redinterglobal.welcome.config.PendingMonitorLoginHint
|
||||
import com.redinterglobal.welcome.config.WelcomeConfig
|
||||
import com.redinterglobal.welcome.domain.EnrolledUser
|
||||
import com.redinterglobal.welcome.domain.EnrolledUserRepo
|
||||
import com.redinterglobal.welcome.service.EnrolledUserSseRegistry
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
import org.springframework.mock.env.MockEnvironment
|
||||
import org.springframework.mock.web.MockHttpSession
|
||||
import org.springframework.ui.ConcurrentModel
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap
|
||||
|
||||
class MonitorControllerTest {
|
||||
|
||||
@Test
|
||||
fun `login exposes remembered monitor email to the view`() {
|
||||
val model = ConcurrentModel()
|
||||
|
||||
assertEquals("monitor/login", controller(mock(EnrolledUserRepo::class.java)).login(
|
||||
model,
|
||||
error = null,
|
||||
monitorEmail = "user@example.com"
|
||||
))
|
||||
|
||||
assertEquals("user@example.com", model.getAttribute("monitorEmail"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `user list omits null repository entries`() {
|
||||
val repository = mock(EnrolledUserRepo::class.java)
|
||||
val user = EnrolledUser("user@example.com", "User", "otp")
|
||||
|
||||
`when`(repository.findAll()).thenReturn(repositoryValues(null, user))
|
||||
|
||||
assertEquals(listOf(user), controller(repository).getUserList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `user list is empty when the repository only contains null entries`() {
|
||||
val repository = mock(EnrolledUserRepo::class.java)
|
||||
|
||||
`when`(repository.findAll()).thenReturn(repositoryValues(null))
|
||||
|
||||
assertEquals(emptyList(), controller(repository).getUserList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `monitor login stores the email as a pending login hint`() {
|
||||
val welcomeConfig = WelcomeConfig(
|
||||
appStores = emptyList(),
|
||||
idp = mapOf("example.com" to "example")
|
||||
)
|
||||
val controller = MonitorController(
|
||||
mock(EnrolledUserRepo::class.java),
|
||||
mock(EnrolledUserSseRegistry::class.java),
|
||||
mock(TurnstileValidationService::class.java),
|
||||
MockEnvironment(),
|
||||
welcomeConfig
|
||||
)
|
||||
|
||||
val session = MockHttpSession()
|
||||
|
||||
assertEquals("redirect:htmx:/oauth2/authorization/example-idP", controller.handleLogin(
|
||||
"user@example.com",
|
||||
RedirectAttributesModelMap(),
|
||||
session
|
||||
))
|
||||
assertEquals(
|
||||
PendingMonitorLoginHint("example-idP", "user@example.com"),
|
||||
session.getAttribute(MonitorLoginHintAuthorizationRequestResolver.PENDING_LOGIN_HINT_SESSION_ATTRIBUTE)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid monitor login clears a pending login hint`() {
|
||||
val session = MockHttpSession().apply {
|
||||
setAttribute(
|
||||
MonitorLoginHintAuthorizationRequestResolver.PENDING_LOGIN_HINT_SESSION_ATTRIBUTE,
|
||||
PendingMonitorLoginHint("example-idP", "user@example.com")
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
"redirect:htmx:/monitor/login",
|
||||
controller(mock(EnrolledUserRepo::class.java)).handleLogin(
|
||||
"invalid-email",
|
||||
RedirectAttributesModelMap(),
|
||||
session
|
||||
)
|
||||
)
|
||||
assertEquals(
|
||||
null,
|
||||
session.getAttribute(MonitorLoginHintAuthorizationRequestResolver.PENDING_LOGIN_HINT_SESSION_ATTRIBUTE)
|
||||
)
|
||||
}
|
||||
|
||||
private fun controller(repository: EnrolledUserRepo) = MonitorController(
|
||||
repository,
|
||||
mock(EnrolledUserSseRegistry::class.java),
|
||||
mock(TurnstileValidationService::class.java),
|
||||
MockEnvironment(),
|
||||
WelcomeConfig(appStores = emptyList()),
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun repositoryValues(vararg values: EnrolledUser?): List<EnrolledUser> =
|
||||
values.toList() as List<EnrolledUser>
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Comunicaciones Productivas Castro y Asociados, S. de R. L. de C.V. - All Rights Reserved
|
||||
*
|
||||
* This source code is protected under international copyright law.
|
||||
* All rights reserved and protected by the copyright holders.
|
||||
* This file is confidential and only available to authorized individuals with the permission of the copyright holders.
|
||||
* If you encounter this file and do not have permission, please contact the copyright holders and delete this file.
|
||||
*/
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import path from 'path';
|
||||
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import ChunksWebpackPlugin from 'chunks-webpack-plugin';
|
||||
import webpack from 'webpack';
|
||||
import autoprefixer from "autoprefixer";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Groups node_modules into one cacheable chunk per top-level package, so that
|
||||
// bumping one dependency does not invalidate the others' [contenthash].
|
||||
const vendorChunkName = (module) => {
|
||||
const matched = module.context?.match(/[\\/]node_modules[\\/]((?:@[^\\/]+[\\/])?[^\\/]+)/);
|
||||
if (!matched) return 'vendor';
|
||||
return `vendor-${matched[1].replace(/@/g, '').replace(/[\\/]/g, '-')}`;
|
||||
};
|
||||
|
||||
export default(_env, argv) => {
|
||||
|
||||
const isProduction = argv.mode === 'production';
|
||||
|
||||
return {
|
||||
|
||||
entry: {
|
||||
monitor: './src/main/resources/js/monitor.js',
|
||||
'login-page': './src/main/resources/js/login-page.js',
|
||||
download: './src/main/resources/js/download.js',
|
||||
otp: './src/main/resources/js/otp.js',
|
||||
enroll: './src/main/resources/js/enroll.js',
|
||||
welcome: './src/main/resources/js/welcome.js',
|
||||
done: './src/main/resources/js/done.js',
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.resolve(__dirname, './build/resources/main/static/dist'),
|
||||
publicPath: '/dist/',
|
||||
// thymepack resolves assets through chunks-manifest.json, so hashed
|
||||
// filenames are transparent to the templates.
|
||||
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
|
||||
chunkFilename: isProduction ? '[name].[contenthash].js' : '[name].js',
|
||||
assetModuleFilename: isProduction ? '[name].[contenthash][ext]' : '[name][ext]',
|
||||
clean: true
|
||||
},
|
||||
|
||||
// Emit source maps as separate assets so browser DevTools can request
|
||||
// them from /dist/ instead of relying on inline eval maps.
|
||||
devtool: isProduction ? false : 'source-map',
|
||||
mode: argv.mode,
|
||||
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
buildDependencies: {
|
||||
config: [__filename]
|
||||
}
|
||||
},
|
||||
|
||||
optimization: {
|
||||
minimize: isProduction,
|
||||
// '...' keeps webpack's default JS minimizer (Terser) with its default options.
|
||||
minimizer: [
|
||||
'...',
|
||||
new CssMinimizerPlugin({
|
||||
minimizerOptions: {
|
||||
preset: ['default', {
|
||||
// Bootstrap uses relative-color calc() expressions that
|
||||
// postcss-calc cannot parse yet. Preserve them for browsers.
|
||||
calc: false
|
||||
}]
|
||||
}
|
||||
})
|
||||
],
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
cacheGroups: {
|
||||
defaultVendors: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: vendorChunkName,
|
||||
reuseExistingChunk: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
performance: {
|
||||
// preline's single 388 KiB module cannot be split; warning on every
|
||||
// build adds no information. Revisit if the payload is trimmed.
|
||||
hints: false
|
||||
},
|
||||
|
||||
devServer: {
|
||||
port: 8081,
|
||||
compress: false,
|
||||
hot: false,
|
||||
liveReload: true,
|
||||
static: {
|
||||
directory: path.resolve(__dirname, './src/main/resources/static'),
|
||||
publicPath: '/',
|
||||
watch: true,
|
||||
},
|
||||
devMiddleware: {
|
||||
writeToDisk: true,
|
||||
},
|
||||
watchFiles: [
|
||||
'./src/main/resources/templates/*.html',
|
||||
'./src/main/resources/templates/**/*.html',
|
||||
'./src/main/resources/messages.properties',
|
||||
'./src/main/resources/messages_*.properties',
|
||||
'./src/main/resources/js/*.js',
|
||||
'./src/main/resources/js/**/*.js',
|
||||
'./src/main/resources/styles/**/*',
|
||||
'./src/main/resources/static/**/*',
|
||||
],
|
||||
proxy: [
|
||||
{
|
||||
context: (pathname) => ![
|
||||
'/dist/',
|
||||
'/img/',
|
||||
'/favicon/',
|
||||
'/.well-known/',
|
||||
'/site.webmanifest',
|
||||
].some((path) => pathname.startsWith(path)),
|
||||
target: "http://localhost:8080",
|
||||
secure: false,
|
||||
prependPath: false,
|
||||
}
|
||||
],
|
||||
headers: {
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
// Consume Bootstrap's shipped maps before webpack creates
|
||||
// its own bundle map. This prevents DevTools from trying
|
||||
// to load Bootstrap's relative *.js.map files at /dist/.
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
include: path.resolve(__dirname, 'node_modules/bootstrap/js/dist'),
|
||||
use: ['source-map-loader']
|
||||
},
|
||||
{ // Bootstrap
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
|
||||
{ loader: 'css-loader' },
|
||||
{ loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
autoprefixer,
|
||||
// ...(isProduction ? [purgeUnusedCss] : [])
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ loader: 'sass-loader' }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
sourceMap: !isProduction
|
||||
}
|
||||
},
|
||||
{
|
||||
// @tailwindcss/postcss resolves @import/@source/@plugin and
|
||||
// handles vendor prefixing through Lightning CSS.
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
autoprefixer
|
||||
]
|
||||
},
|
||||
sourceMap: !isProduction
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.woff2?$/,
|
||||
type: "asset/resource",
|
||||
},
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
// @qr-platform/qr-code.js includes a Node-only dynamic Buffer import.
|
||||
// Its browser runtime never reaches that path, but Webpack still resolves it.
|
||||
fallback: {
|
||||
buffer: false,
|
||||
},
|
||||
modules: [
|
||||
path.resolve(__dirname, 'src/main/resources'),
|
||||
'node_modules'
|
||||
],
|
||||
extensions: [ '...' ],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
// _: 'lodash',
|
||||
}),
|
||||
new ChunksWebpackPlugin({
|
||||
generateChunksManifest: true,
|
||||
generateChunksFiles: false
|
||||
}),
|
||||
...(isProduction
|
||||
? [new MiniCssExtractPlugin({
|
||||
filename: '[name].[contenthash].css',
|
||||
chunkFilename: '[name].[contenthash].css'
|
||||
})]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
};
|
||||