commit f2173a9fa9ebd947f232dc30b8f7bf8ec72b185c Author: Eric <01714308@yto.net.cn> Date: Mon Feb 9 11:24:51 2026 +0800 jdk 17 diff --git a/demo/backend/.gitattributes b/demo/backend/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/demo/backend/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/demo/backend/.gitignore b/demo/backend/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/demo/backend/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/demo/backend/mvnw b/demo/backend/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/demo/backend/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/demo/backend/mvnw.cmd b/demo/backend/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/demo/backend/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/demo/backend/pom.xml b/demo/backend/pom.xml new file mode 100644 index 0000000..3e3d6cb --- /dev/null +++ b/demo/backend/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.10 + + + org.lingniu + demo + 0.0.1-SNAPSHOT + demo + demo + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.lingniu + oauth2-login-sdk + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/demo/backend/src/main/java/com/example/demo/DemoApplication.java b/demo/backend/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..70629e9 --- /dev/null +++ b/demo/backend/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = {"org.lingniu.**"}) +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} diff --git a/demo/backend/src/main/java/com/example/demo/test.http b/demo/backend/src/main/java/com/example/demo/test.http new file mode 100644 index 0000000..4a0bfa1 --- /dev/null +++ b/demo/backend/src/main/java/com/example/demo/test.http @@ -0,0 +1,41 @@ +# @no-redirect +GET localhost:10001/oauth2/authorization/demo + +### +POST http://localhost:8000/oauth2/authorize +Content-Type: application/x-www-form-urlencoded +Accept: application/json +Idp: 387cec08371f4ebfb61074d41a94046e +Cookie: idp_refresh_token=7bb21a0dcac94aec99f08ae6a2d6db30 + +response_type=code&client_id=b55c88c20db94790a60a5075&scope=openid%20profile%20perms&state=MK1s_JKXsVowOsaGIGK3UK00yVgjUM-lgV-T7tOZdIQ%3D&redirect_uri=http://localhost:9506/oauth2/callback&nonce=rXdsOr0tczTckUSP_RKZ5ABmP575Z4JrTLOxCQ1nt3U + + +#### +GET http://localhost:10001/login/oauth2/code/demo?code=ua3zRRX2YMHsGmYaY4CGEvtklZbCzNtT5sOjguXzhY68zoKqnA83NlQXtG1dN-X_mv4Sn5MaYERkymxk9EWJzpHA_RB523keRb25jmIt5LgUjWJtwD4gJmQJulPOXFO1&state=MK1s_JKXsVowOsaGIGK3UK00yVgjUM-lgV-T7tOZdIQ%3D + +### +GET http://localhost:10001/idp/routes +#Authorization: 85a9f4d6fef34763b4437830ec331570 +Authorization: Bearer eyJraWQiOiJpZHAiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImI1NWM4OGMyMGRiOTQ3OTBhNjBhNTA3NSIsIm5iZiI6MTc3MDM5MjMwMiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsInBlcm1zIl0sImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImV4cCI6MTc3MDQzNTUwMiwiaWF0IjoxNzcwMzkyMzAyLCJqdGkiOiIxMmRjZjZmOS0zMjNhLTRhMmUtYjI4Ni1lNDcyOTFhNjc4YTYifQ.MC2khfn7Q2PeU5NB9BCazj-4oWsS_9VIoRLvVZfRiM4RKyAw6VkBv0bNWNuIcUAzZ7GpfIsGMufjsDiVgj7tBK_MWweasWz7DRDc_QCkFt8RZxK2LjxZAilFmXZOaydUNnlGgBmI6S-xAD5N5ltx8OTEdWHuD7tm7S8ppXlvTCk4QSeNd3UYXyXPkR408HOk5ZWTH4PudGVJN5q5gDUAbM9FyN7NejGuJQ4gmHuur7oDhMEqmBQjiv6OnJZko6GszOcN0-nkRJX-KzXV45uIkEF9BaUhJvC6EhotqioVXLuLznX3yB9iuFGqekpS3uHOYwzZF0CHR6xTHg29hvLOxw +#Cookie: app_refresh_token=ce08d9a6b3064311ac163a7806b811ef + + +### +GET http://localhost:8000/account/getRouters +#Authorization: 85a9f4d6fef34763b4437830ec331570 +Authorization: Bearer eyJraWQiOiJpZHAiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImI1NWM4OGMyMGRiOTQ3OTBhNjBhNTA3NSIsIm5iZiI6MTc3MDM5MjMwMiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsInBlcm1zIl0sImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImV4cCI6MTc3MDQzNTUwMiwiaWF0IjoxNzcwMzkyMzAyLCJqdGkiOiIxMmRjZjZmOS0zMjNhLTRhMmUtYjI4Ni1lNDcyOTFhNjc4YTYifQ.MC2khfn7Q2PeU5NB9BCazj-4oWsS_9VIoRLvVZfRiM4RKyAw6VkBv0bNWNuIcUAzZ7GpfIsGMufjsDiVgj7tBK_MWweasWz7DRDc_QCkFt8RZxK2LjxZAilFmXZOaydUNnlGgBmI6S-xAD5N5ltx8OTEdWHuD7tm7S8ppXlvTCk4QSeNd3UYXyXPkR408HOk5ZWTH4PudGVJN5q5gDUAbM9FyN7NejGuJQ4gmHuur7oDhMEqmBQjiv6OnJZko6GszOcN0-nkRJX-KzXV45uIkEF9BaUhJvC6EhotqioVXLuLznX3yB9iuFGqekpS3uHOYwzZF0CHR6xTHg29hvLOxw +#Cookie: app_refresh_token=ce08d9a6b3064311ac163a7806b811ef +Accept: application/json + +#### +GET http://localhost:10001/idp/routes +#Authorization: 85a9f4d6fef34763b4437830ec331570 +Authorization: 41deb286d03b42139bc3559cbbcc9995 +#Cookie: app_refresh_token=ce08d9a6b3064311ac163a7806b811ef +Accept: application/json + +### +POST http://localhost:10001/logout +Cookie: app_refresh_token=02237ce2c5d14e8088be3d462b69df99 + diff --git a/demo/backend/src/main/resources/application.yml b/demo/backend/src/main/resources/application.yml new file mode 100644 index 0000000..611d34a --- /dev/null +++ b/demo/backend/src/main/resources/application.yml @@ -0,0 +1,70 @@ +spring: + application: + name: demo + # redis \u914D\u7F6E + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: http://localhost:8000/oauth2/jwks + client: + registration: + demo: + client-id: b55c88c20db94790a60a5075 + client-secret: UqVAS8UiehSFJSR8_CygnYGR5M79LuGuGiDwATtcGqg + client-name: DEMO + authorization-grant-type: authorization_code + redirect-uri: http://localhost:9506/oauth2/callback + scope: + - openid + - profile + # 返回权限 + - perms + provider: idp + + provider: + idp: +# issuer-uri: http://localhost:8000 + authorization-uri: http://localhost/sso + token-uri: http://localhost:8000/oauth2/token + user-info-uri: http://localhost:8000/userinfo + jwk-set-uri: http://localhost:8000/oauth2/jwks + user-name-attribute: sub + data: + redis: + # \u5730\u5740 + host: localhost + # \u7AEF\u53E3\uFF0C\u9ED8\u8BA4\u4E3A6379 + port: 6379 + # \u6570\u636E\u5E93\u7D22\u5F15 + database: 0 + # \u5BC6\u7801 + password: + # \u8FDE\u63A5\u8D85\u65F6\u65F6\u95F4 + timeout: 10s + lettuce: + pool: + # \u8FDE\u63A5\u6C60\u4E2D\u7684\u6700\u5C0F\u7A7A\u95F2\u8FDE\u63A5 + min-idle: 0 + # \u8FDE\u63A5\u6C60\u4E2D\u7684\u6700\u5927\u7A7A\u95F2\u8FDE\u63A5 + max-idle: 8 + # \u8FDE\u63A5\u6C60\u7684\u6700\u5927\u6570\u636E\u5E93\u8FDE\u63A5\u6570 + max-active: 8 + # #\u8FDE\u63A5\u6C60\u6700\u5927\u963B\u585E\u7B49\u5F85\u65F6\u95F4\uFF08\u4F7F\u7528\u8D1F\u503C\u8868\u793A\u6CA1\u6709\u9650\u5236\uFF09 + max-wait: -1ms + + + + + +logging: + level: + root: info + org.springframework.web: debug + org.springframework.security: debug + org.springframework.security.oauth2: debug + + +server: + port: 10001 + diff --git a/demo/backend/src/test/java/com/example/demo/DemoApplicationTests.java b/demo/backend/src/test/java/com/example/demo/DemoApplicationTests.java new file mode 100644 index 0000000..eaa9969 --- /dev/null +++ b/demo/backend/src/test/java/com/example/demo/DemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DemoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/demo/frontend/.gitignore b/demo/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/demo/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/demo/frontend/README.md b/demo/frontend/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/demo/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/demo/frontend/package.json b/demo/frontend/package.json new file mode 100644 index 0000000..35b915a --- /dev/null +++ b/demo/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "demo-sdk", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "oauth2-login-sdk": "link:C:/Users/admin/AppData/Local/pnpm/global/5/node_modules/oauth2-login-sdk", + "vue": "^3.5.24", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "typescript": "~5.9.3", + "vite": "^7.2.4", + "vue-tsc": "^3.1.4" + } +} diff --git a/demo/frontend/pnpm-lock.yaml b/demo/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..621cd77 --- /dev/null +++ b/demo/frontend/pnpm-lock.yaml @@ -0,0 +1,976 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + oauth2-login-sdk: link:C:/Users/admin/AppData/Local/pnpm/global/5/node_modules/oauth2-login-sdk + +importers: + + .: + dependencies: + oauth2-login-sdk: + specifier: link:C:/Users/admin/AppData/Local/pnpm/global/5/node_modules/oauth2-login-sdk + version: link:C:/Users/admin/AppData/Local/pnpm/global/5/node_modules/oauth2-login-sdk + vue: + specifier: ^3.5.24 + version: 3.5.27(typescript@5.9.3) + vue-router: + specifier: ^4.3.0 + version: 4.6.4(vue@3.5.27(typescript@5.9.3)) + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.10.10 + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.4(vite@7.3.1(@types/node@24.10.10))(vue@3.5.27(typescript@5.9.3)) + '@vue/tsconfig': + specifier: ^0.8.1 + version: 0.8.1(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.2.4 + version: 7.3.1(@types/node@24.10.10) + vue-tsc: + specifier: ^3.1.4 + version: 3.2.4(typescript@5.9.3) + +packages: + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rolldown/pluginutils@1.0.0-rc.2': + resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} + + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@24.10.10': + resolution: {integrity: sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==} + + '@vitejs/plugin-vue@6.0.4': + resolution: {integrity: sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.2.25 + + '@volar/language-core@2.4.27': + resolution: {integrity: sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==} + + '@volar/source-map@2.4.27': + resolution: {integrity: sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==} + + '@volar/typescript@2.4.27': + resolution: {integrity: sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==} + + '@vue/compiler-core@3.5.27': + resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==} + + '@vue/compiler-dom@3.5.27': + resolution: {integrity: sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==} + + '@vue/compiler-sfc@3.5.27': + resolution: {integrity: sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==} + + '@vue/compiler-ssr@3.5.27': + resolution: {integrity: sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/language-core@3.2.4': + resolution: {integrity: sha512-bqBGuSG4KZM45KKTXzGtoCl9cWju5jsaBKaJJe3h5hRAAWpZUuj5G+L+eI01sPIkm4H6setKRlw7E85wLdDNew==} + + '@vue/reactivity@3.5.27': + resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==} + + '@vue/runtime-core@3.5.27': + resolution: {integrity: sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==} + + '@vue/runtime-dom@3.5.27': + resolution: {integrity: sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==} + + '@vue/server-renderer@3.5.27': + resolution: {integrity: sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==} + peerDependencies: + vue: 3.5.27 + + '@vue/shared@3.5.27': + resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==} + + '@vue/tsconfig@0.8.1': + resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + alien-signals@3.1.2: + resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-tsc@3.2.4: + resolution: {integrity: sha512-xj3YCvSLNDKt1iF9OcImWHhmYcihVu9p4b9s4PGR/qp6yhW+tZJaypGxHScRyOrdnHvaOeF+YkZOdKwbgGvp5g==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.27: + resolution: {integrity: sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rolldown/pluginutils@1.0.0-rc.2': {} + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@types/estree@1.0.8': {} + + '@types/node@24.10.10': + dependencies: + undici-types: 7.16.0 + + '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@24.10.10))(vue@3.5.27(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.2 + vite: 7.3.1(@types/node@24.10.10) + vue: 3.5.27(typescript@5.9.3) + + '@volar/language-core@2.4.27': + dependencies: + '@volar/source-map': 2.4.27 + + '@volar/source-map@2.4.27': {} + + '@volar/typescript@2.4.27': + dependencies: + '@volar/language-core': 2.4.27 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.27': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.27 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.27': + dependencies: + '@vue/compiler-core': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/compiler-sfc@3.5.27': + dependencies: + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.5.27 + '@vue/compiler-dom': 3.5.27 + '@vue/compiler-ssr': 3.5.27 + '@vue/shared': 3.5.27 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.27': + dependencies: + '@vue/compiler-dom': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/devtools-api@6.6.4': {} + + '@vue/language-core@3.2.4': + dependencies: + '@volar/language-core': 2.4.27 + '@vue/compiler-dom': 3.5.27 + '@vue/shared': 3.5.27 + alien-signals: 3.1.2 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + + '@vue/reactivity@3.5.27': + dependencies: + '@vue/shared': 3.5.27 + + '@vue/runtime-core@3.5.27': + dependencies: + '@vue/reactivity': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/runtime-dom@3.5.27': + dependencies: + '@vue/reactivity': 3.5.27 + '@vue/runtime-core': 3.5.27 + '@vue/shared': 3.5.27 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.27(vue@3.5.27(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.27 + '@vue/shared': 3.5.27 + vue: 3.5.27(typescript@5.9.3) + + '@vue/shared@3.5.27': {} + + '@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))': + optionalDependencies: + typescript: 5.9.3 + vue: 3.5.27(typescript@5.9.3) + + alien-signals@3.1.2: {} + + csstype@3.2.3: {} + + entities@7.0.1: {} + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + estree-walker@2.0.2: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + path-browserify@1.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + source-map-js@1.2.1: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + vite@7.3.1(@types/node@24.10.10): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.10 + fsevents: 2.3.3 + + vscode-uri@3.1.0: {} + + vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.27(typescript@5.9.3) + + vue-tsc@3.2.4(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.27 + '@vue/language-core': 3.2.4 + typescript: 5.9.3 + + vue@3.5.27(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.27 + '@vue/compiler-sfc': 3.5.27 + '@vue/runtime-dom': 3.5.27 + '@vue/server-renderer': 3.5.27(vue@3.5.27(typescript@5.9.3)) + '@vue/shared': 3.5.27 + optionalDependencies: + typescript: 5.9.3 diff --git a/demo/frontend/pnpm-workspace.yaml b/demo/frontend/pnpm-workspace.yaml new file mode 100644 index 0000000..77e2017 --- /dev/null +++ b/demo/frontend/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +overrides: + oauth2-login-sdk: link:C:/Users/admin/AppData/Local/pnpm/global/5/node_modules/oauth2-login-sdk diff --git a/demo/frontend/public/vite.svg b/demo/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/demo/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/frontend/src/App.vue b/demo/frontend/src/App.vue new file mode 100644 index 0000000..59b75db --- /dev/null +++ b/demo/frontend/src/App.vue @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/demo/frontend/src/assets/vue.svg b/demo/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/demo/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/frontend/src/components/Home.vue b/demo/frontend/src/components/Home.vue new file mode 100644 index 0000000..cb56f8a --- /dev/null +++ b/demo/frontend/src/components/Home.vue @@ -0,0 +1,37 @@ + + + \ No newline at end of file diff --git a/demo/frontend/src/main.ts b/demo/frontend/src/main.ts new file mode 100644 index 0000000..cd35530 --- /dev/null +++ b/demo/frontend/src/main.ts @@ -0,0 +1,18 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' +import router from './router' +import unifiedLoginSDK from "oauth2-login-sdk"; + +// 初始化配置 +unifiedLoginSDK.init({ + clientId: 'b55c88c20db94790a60a5075', + registrationId: 'demo', + storageType: 'localStorage', + basepath: '/demo-api', + idpLogoutUrl: 'http://localhost/logout', + homePage: 'http://localhost:9506/home' +}); +const app = createApp(App) +app.use(router) +app.mount('#app') diff --git a/demo/frontend/src/router/index.ts b/demo/frontend/src/router/index.ts new file mode 100644 index 0000000..6e39902 --- /dev/null +++ b/demo/frontend/src/router/index.ts @@ -0,0 +1,31 @@ +import { createRouter, createWebHistory } from 'vue-router' +import unifiedLoginSDK from 'oauth2-login-sdk'; +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'home', + redirect: '/home' + }, + { + path: '/', + name: 'home', + component: () => import('../components/Home.vue') + } + ] +}) +router.beforeEach(async (to, _from, next) => { + debugger + if (!unifiedLoginSDK.isAuthenticated()) { + if (to.path === '/oauth2/callback') { + await unifiedLoginSDK.handleCallback() + }else{ + await unifiedLoginSDK.login() + } + } else { + next() + } +}) + +export default router \ No newline at end of file diff --git a/demo/frontend/src/style.css b/demo/frontend/src/style.css new file mode 100644 index 0000000..f691315 --- /dev/null +++ b/demo/frontend/src/style.css @@ -0,0 +1,79 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/demo/frontend/tsconfig.app.json b/demo/frontend/tsconfig.app.json new file mode 100644 index 0000000..8d16e42 --- /dev/null +++ b/demo/frontend/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/demo/frontend/tsconfig.json b/demo/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/demo/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/demo/frontend/tsconfig.node.json b/demo/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/demo/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/demo/frontend/vite.config.ts b/demo/frontend/vite.config.ts new file mode 100644 index 0000000..81e0f7e --- /dev/null +++ b/demo/frontend/vite.config.ts @@ -0,0 +1,85 @@ +import { defineConfig, loadEnv } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => { + // 加载环境变量 + const env = loadEnv(mode, process.cwd(), '') + + // 定义基础 URL,支持环境变量 + const baseUrl = env.VITE_API_BASE_URL || 'http://localhost:10001' + const apiPrefix = env.VITE_API_PREFIX || '/demo-api' + + return { + plugins: [vue()], + + // 基础路径 + base: env.VITE_BASE_PATH || '/', + + // 解析配置 + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + extensions: ['.js', '.ts', '.jsx', '.tsx', '.vue', '.json'] + }, + + // 开发服务器配置 + server: { + port: 9506, + host: true, // 监听所有地址 + open: true, // 自动打开浏览器 + cors: true, // 启用 CORS + + // 代理配置 + proxy: { + // API 代理(修正了 rewrite 路径) + [apiPrefix]: { + target: baseUrl, + changeOrigin: true, + rewrite: (path) => path.replace(new RegExp(`^${apiPrefix}`), '') + }, + + // WebSocket 代理(如果需要) + '/ws': { + target: baseUrl.replace('http', 'ws'), + ws: true, + changeOrigin: true + } + } + }, + + // 构建配置 + build: { + outDir: 'dist', + sourcemap: mode !== 'production', + chunkSizeWarningLimit: 1600, + rollupOptions: { + output: { + manualChunks: { + vue: ['vue', 'vue-router', 'vuex/pinia'], + vendor: ['axios', 'lodash', 'dayjs'], + ui: ['element-plus', 'ant-design-vue'] // 根据实际使用的 UI 库调整 + } + } + } + }, + + // 预览配置 + preview: { + port: 9507, + host: true, + open: true + }, + + // CSS 配置 + css: { + preprocessorOptions: { + scss: { + additionalData: `@import "@/styles/variables.scss";` + } + } + } + } +}) \ No newline at end of file diff --git a/idp/backend/idp-starter/.gitattributes b/idp/backend/idp-starter/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/idp/backend/idp-starter/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/idp/backend/idp-starter/.gitignore b/idp/backend/idp-starter/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/idp/backend/idp-starter/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/idp/backend/idp-starter/mvnw b/idp/backend/idp-starter/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/idp/backend/idp-starter/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/idp/backend/idp-starter/mvnw.cmd b/idp/backend/idp-starter/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/idp/backend/idp-starter/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/idp/backend/idp-starter/pom.xml b/idp/backend/idp-starter/pom.xml new file mode 100644 index 0000000..e4c6c9b --- /dev/null +++ b/idp/backend/idp-starter/pom.xml @@ -0,0 +1,168 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.10 + + + org.lingniu + idp-starter + 0.0.1-SNAPSHOT + idp-starter + idp-starter + + + + + + + + + + + + + + + 17 + 1.5.5 + 8.0.33 + 3.5.10 + 5.8.40 + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-oauth2-authorization-server + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.session + spring-session-data-redis + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + + + cn.hutool + hutool-all + ${hutool.version} + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.fasterxml.jackson.core + jackson-core + 2.19.4 + compile + + + com.fasterxml.jackson.core + jackson-databind + 2.19.4 + compile + + + org.apache.commons + commons-lang3 + 3.20.0 + compile + + + com.alibaba.fastjson2 + fastjson2 + 2.0.60 + compile + + + org.springframework.boot + spring-boot-starter-validation + + + pro.fessional + kaptcha + 2.3.3 + compile + + + nl.basjes.parse.useragent + yauaa + 7.32.0 + compile + + + mysql + mysql-connector-java + ${mysql.version} + + + + + + com.nimbusds + nimbus-jose-jwt + 10.0.2 + compile + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/IdpStarterApplication.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/IdpStarterApplication.java new file mode 100644 index 0000000..245a877 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/IdpStarterApplication.java @@ -0,0 +1,15 @@ +package org.lingniu.idp; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan("org.lingniu.**.mapper") +public class IdpStarterApplication { + + public static void main(String[] args) { + SpringApplication.run(IdpStarterApplication.class, args); + } + +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/Anonymous.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/Anonymous.java new file mode 100644 index 0000000..61b01a3 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/Anonymous.java @@ -0,0 +1,15 @@ +package org.lingniu.idp.annotation; + +import java.lang.annotation.*; + +/** + * 匿名访问不鉴权注解 + * + * @author portal + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Anonymous +{ +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/DataScope.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/DataScope.java new file mode 100644 index 0000000..8637d3a --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/DataScope.java @@ -0,0 +1,29 @@ +package org.lingniu.idp.annotation; + +import java.lang.annotation.*; + +/** + * 数据权限过滤注解 + * + * @author portal + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataScope +{ + /** + * 部门表的别名 + */ + public String deptAlias() default ""; + + /** + * 用户表的别名 + */ + public String userAlias() default ""; + + /** + * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来 + */ + public String permission() default ""; +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/DataSource.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/DataSource.java new file mode 100644 index 0000000..bb29148 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/DataSource.java @@ -0,0 +1,24 @@ +package org.lingniu.idp.annotation; + +import org.lingniu.idp.enums.DataSourceType; + +import java.lang.annotation.*; + +/** + * 自定义多数据源切换注解 + * + * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 + * + * @author portal + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface DataSource +{ + /** + * 切换数据源名称 + */ + public DataSourceType value() default DataSourceType.MASTER; +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/Log.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/Log.java new file mode 100644 index 0000000..8387885 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/Log.java @@ -0,0 +1,48 @@ +package org.lingniu.idp.annotation; + +import org.lingniu.idp.enums.BusinessType; +import org.lingniu.idp.enums.OperatorType; + +import java.lang.annotation.*; + +/** + * 自定义操作日志记录注解 + * + * @author portal + * + */ +@Target({ ElementType.PARAMETER, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log +{ + /** + * 模块 + */ + public String title() default ""; + + /** + * 功能 + */ + public BusinessType businessType() default BusinessType.OTHER; + + /** + * 操作人类别 + */ + public OperatorType operatorType() default OperatorType.MANAGE; + + /** + * 是否保存请求的参数 + */ + public boolean isSaveRequestData() default true; + + /** + * 是否保存响应的参数 + */ + public boolean isSaveResponseData() default true; + + /** + * 排除指定的请求参数 + */ + public String[] excludeParamNames() default {}; +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/RateLimiter.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/RateLimiter.java new file mode 100644 index 0000000..144810e --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/RateLimiter.java @@ -0,0 +1,37 @@ +package org.lingniu.idp.annotation; + +import org.lingniu.idp.constant.CacheConstants; +import org.lingniu.idp.enums.LimitType; + +import java.lang.annotation.*; + +/** + * 限流注解 + * + * @author portal + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RateLimiter +{ + /** + * 限流key + */ + public String key() default CacheConstants.RATE_LIMIT_KEY; + + /** + * 限流时间,单位秒 + */ + public int time() default 60; + + /** + * 限流次数 + */ + public int count() default 100; + + /** + * 限流类型 + */ + public LimitType limitType() default LimitType.DEFAULT; +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/RepeatSubmit.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/RepeatSubmit.java new file mode 100644 index 0000000..72a21e4 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/RepeatSubmit.java @@ -0,0 +1,26 @@ +package org.lingniu.idp.annotation; + +import java.lang.annotation.*; + +/** + * 自定义注解防止表单重复提交 + * + * @author portal + * + */ +@Inherited +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepeatSubmit +{ + /** + * 间隔时间(ms),小于此时间视为重复提交 + */ + public int interval() default 5000; + + /** + * 提示消息 + */ + public String message() default "不允许重复提交,请稍候再试"; +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/Sensitive.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/Sensitive.java new file mode 100644 index 0000000..a7e95cf --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/annotation/Sensitive.java @@ -0,0 +1,25 @@ +package org.lingniu.idp.annotation; + +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.lingniu.idp.config.serializer.SensitiveJsonSerializer; +import org.lingniu.idp.enums.DesensitizedType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 数据脱敏注解 + * + * @author portal + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@JacksonAnnotationsInside +@JsonSerialize(using = SensitiveJsonSerializer.class) +public @interface Sensitive +{ + DesensitizedType desensitizedType(); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/common/redis/RedisCache.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/common/redis/RedisCache.java new file mode 100644 index 0000000..530c14d --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/common/redis/RedisCache.java @@ -0,0 +1,273 @@ +package org.lingniu.idp.common.redis; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.BoundSetOperations; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * spring redis 工具类 + * + * @author portal + **/ +@SuppressWarnings(value = { "unchecked", "rawtypes" }) +@Component +public class RedisCache +{ + @Autowired + public RedisTemplate redisTemplate; + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) + { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) + { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) + { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) + { + return redisTemplate.expire(key, timeout, unit); + } + + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) + { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) + { + return redisTemplate.hasKey(key); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) + { + ValueOperations operation = redisTemplate.opsForValue(); + return operation.get(key); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) + { + return redisTemplate.delete(key); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public boolean deleteObject(final Collection collection) + { + return redisTemplate.delete(collection) > 0; + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long setCacheList(final String key, final List dataList) + { + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) + { + return redisTemplate.opsForList().range(key, 0, -1); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public BoundSetOperations setCacheSet(final String key, final Set dataSet) + { + BoundSetOperations setOperation = redisTemplate.boundSetOps(key); + Iterator it = dataSet.iterator(); + while (it.hasNext()) + { + setOperation.add(it.next()); + } + return setOperation; + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) + { + return redisTemplate.opsForSet().members(key); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) + { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) + { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) + { + redisTemplate.opsForHash().put(key, hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) + { + HashOperations opsForHash = redisTemplate.opsForHash(); + return opsForHash.get(key, hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) + { + return redisTemplate.opsForHash().multiGet(key, hKeys); + } + + /** + * 删除Hash中的某条数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return 是否成功 + */ + public boolean deleteCacheMapValue(final String key, final String hKey) + { + return redisTemplate.opsForHash().delete(key, hKey) > 0; + } + public boolean deleteCacheSetValue(final String key, final String hKey) + { + return redisTemplate.opsForSet().remove(key, hKey) > 0; + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) + { + return redisTemplate.keys(pattern); + } + + public void setCacheSet(String key, String value) { + redisTemplate.opsForSet().add(key,value); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/common/xss/Xss.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/common/xss/Xss.java new file mode 100644 index 0000000..74fb9e3 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/common/xss/Xss.java @@ -0,0 +1,29 @@ +package org.lingniu.idp.common.xss; + + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义xss校验注解 + * + * @author portal + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) +@Constraint(validatedBy = { XssValidator.class }) +public @interface Xss +{ + String message() + + default "不允许任何脚本运行"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/common/xss/XssValidator.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/common/xss/XssValidator.java new file mode 100644 index 0000000..d70b912 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/common/xss/XssValidator.java @@ -0,0 +1,40 @@ +package org.lingniu.idp.common.xss; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.lingniu.idp.utils.StringUtils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 自定义xss校验注解实现 + * + * @author portal + */ +public class XssValidator implements ConstraintValidator +{ + private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) + { + if (StringUtils.isBlank(value)) + { + return true; + } + return !containsHtml(value); + } + + public static boolean containsHtml(String value) + { + StringBuilder sHtml = new StringBuilder(); + Pattern pattern = Pattern.compile(HTML_PATTERN); + Matcher matcher = pattern.matcher(value); + while (matcher.find()) + { + sHtml.append(matcher.group()); + } + return pattern.matcher(sHtml).matches(); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/AuthorizationServerConfig.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/AuthorizationServerConfig.java new file mode 100644 index 0000000..6c13364 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/AuthorizationServerConfig.java @@ -0,0 +1,202 @@ +package org.lingniu.idp.config; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.lingniu.idp.enums.CustomScopes; +import org.lingniu.idp.security.filter.IdpAuthenticationFilter; +import org.lingniu.idp.security.granttype.SmsCodeGrantType; +import org.lingniu.idp.security.handler.Oauth2CodeSuccessHandler; +import org.lingniu.idp.service.core.UserDetailsServiceImpl; +import org.lingniu.idp.service.core.login.IdpTokenService; +import org.lingniu.idp.service.core.login.LoginService; +import org.lingniu.idp.service.core.login.RedisAccessTokenService; +import org.lingniu.idp.utils.jwt.Jwks; +import org.lingniu.idp.utils.jwt.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +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.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext; +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Duration; +import java.util.UUID; +import java.util.function.Function; + +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer.authorizationServer; + +@Configuration +public class AuthorizationServerConfig { + +// @Autowired +// private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider; + private static final String CUSTOM_CONSENT_PAGE_URI = "http://localhost/oauth2/consent"; + @Autowired + private JwtUtil jwtUtil; + @Autowired + private IdpTokenService idpTokenService; + @Autowired + private UserDetailsServiceImpl userDetailsService; + @Autowired + private OidcUserInfoMapper oidcUserInfoMapper; + + @Bean + @Order(1) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = authorizationServer(); + http + .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) + .with(authorizationServerConfigurer, (authorizationServer) -> + authorizationServer + .authorizationEndpoint(authorizationEndpoint -> + authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI) + .authorizationResponseHandler(new Oauth2CodeSuccessHandler()) + ) + .tokenEndpoint(oAuth2TokenEndpointConfigurer -> {}) + .oidc(Customizer.withDefaults()) + .oidc(oidcConfigurer -> + oidcConfigurer.userInfoEndpoint(oidcUserInfoEndpointConfigurer -> + oidcUserInfoEndpointConfigurer.userInfoMapper(oidcUserInfoMapper) + ) + ) // Enable OpenID Connect 1.0 + ) + .addFilterAfter(jwtAuthenticationFilter(), LogoutFilter.class) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer.jwt(Customizer.withDefaults())) + .authorizeHttpRequests((authorize) -> + authorize.anyRequest().authenticated() + ) + // Redirect to the /login page when not authenticated from the authorization endpoint + // NOTE: DefaultSecurityConfig is configured with formLogin.loginPage("/login") + .exceptionHandling((exceptions) -> exceptions + .defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint("http://localhost/login"), + new MediaTypeRequestMatcher(MediaType.TEXT_HTML) + ) + ); + return http.build(); + } + @Bean + public JdbcRegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { + RegisteredClient client = RegisteredClient.withId("68dc155a-1034-4cd8-91dc-fca206e2f6d3") + .clientId("2c6f1d9ff78641c78d72a848") + //l3X95am7RaX8-Uu9-5nDYmD9-OFU-8_GHmkfnKfaS_0 + .clientSecret("{bcrypt}$2a$10$JDoQUK5aWvYiZZDpgSC8k.qPGfqbYtXAF5H1IW/e2quGWA8Zr6ffq") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .authorizationGrantType(SmsCodeGrantType.SMS_CODE) // 添加自定义Grant Type + .clientName("PORTAL") + .redirectUri("http://localhost:81/callback") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .scope(CustomScopes.PERMS.name().toLowerCase()) + .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(12)).refreshTokenTimeToLive(Duration.ofDays(7)).build()) + .scope("message.read") + .scope("message.write") + .build(); + RegisteredClient demo = RegisteredClient.withId("c81008aa-b33f-4ebc-bba7-3c6d0eee23b9") + .clientId("b55c88c20db94790a60a5075") + //l3X95am7RaX8-Uu9-5nDYmD9-OFU-8_GHmkfnKfaS_0 + .clientSecret("{bcrypt}$2a$10$gLsAsObi5ffOddmkf5YchOtT3olfcAmSTt75X6H/mHBVYf8DXnIwi") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .authorizationGrantType(SmsCodeGrantType.SMS_CODE) // 添加自定义Grant Type + .clientName("DEMO") + .redirectUri("http://localhost:9506/oauth2/callback") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .scope(CustomScopes.PERMS.name().toLowerCase()) + .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(12)).refreshTokenTimeToLive(Duration.ofDays(7)).build()) + .scope("read") + .scope("write") + .build(); + // Save registered client's in db as if in-memory + JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); + registeredClientRepository.save(client); + registeredClientRepository.save(demo); + return registeredClientRepository; + } + +// @Bean +// public OAuth2TokenGenerator tokenGenerator(CustomAccessTokenGenerator accessTokenGenerator,OAuth2RefreshTokenGenerator refreshTokenGenerator) { +// CustomAccessTokenGenerator accessTokenGenerator = new CustomAccessTokenGenerator(); +// OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); +// return new DelegatingOAuth2TokenGenerator( +// accessTokenGenerator, refreshTokenGenerator +// ); +// } +@Bean +public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate) { + return new JdbcOAuth2AuthorizationService(jdbcTemplate,registeredClientRepository(jdbcTemplate)); +} + + /** + * 自定义JWT认证过滤器 + */ + @Bean + public IdpAuthenticationFilter jwtAuthenticationFilter() { + return new IdpAuthenticationFilter(idpTokenService,userDetailsService); + } + /** + * 主JWK源,使用应用配置的RSA密钥 + */ + @Bean + public JWKSource jwkSource() { + try { + // 从JwtUtil中获取密钥 + RSAPublicKey publicKey = jwtUtil.getPublicKey(); + RSAPrivateKey privateKey = jwtUtil.getPrivateKey(); + + // 创建RSA JWK,包含私钥(用于签名) + RSAKey rsaKey = Jwks.generateRsa( + publicKey, + privateKey, + "idp" // 密钥ID + ); + + JWKSet jwkSet = new JWKSet(rsaKey); + + // 使用ImmutableJWKSet包装 + return new ImmutableJWKSet<>(jwkSet); + + } catch (Exception e) { + throw new RuntimeException("初始化JWK源失败", e); + } + } + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/CaptchaConfig.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/CaptchaConfig.java new file mode 100644 index 0000000..ef25bdf --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/CaptchaConfig.java @@ -0,0 +1,85 @@ +package org.lingniu.idp.config; + +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + * + * @author portal + */ +@Configuration +public class CaptchaConfig +{ + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + @Bean(name = "captchaProducerMath") + public DefaultKaptcha getKaptchaBeanMath() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 边框颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); + // 验证码文本生成器 + properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "org.lingniu.idp.config.KaptchaTextCreator"); + // 验证码文本字符间距 默认为2 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 验证码噪点颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); + // 干扰实现类 + properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/I18nConfig.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/I18nConfig.java new file mode 100644 index 0000000..ca16a66 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/I18nConfig.java @@ -0,0 +1,43 @@ +package org.lingniu.idp.config; + +import org.lingniu.idp.constant.Constants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.servlet.i18n.SessionLocaleResolver; + +/** + * 资源文件配置加载 + * + * @author portal + */ +@Configuration +public class I18nConfig implements WebMvcConfigurer +{ + @Bean + public LocaleResolver localeResolver() + { + SessionLocaleResolver slr = new SessionLocaleResolver(); + // 默认语言 + slr.setDefaultLocale(Constants.DEFAULT_LOCALE); + return slr; + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() + { + LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); + // 参数名 + lci.setParamName("lang"); + return lci; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(localeChangeInterceptor()); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/JwtProperties.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/JwtProperties.java new file mode 100644 index 0000000..49f2276 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/JwtProperties.java @@ -0,0 +1,67 @@ +package org.lingniu.idp.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import java.time.Duration; + +@Component +@ConfigurationProperties(prefix = "idp.jwt") +public class JwtProperties { + + // 令牌配置 + private String header = "Authorization"; + private String prefix = "Bearer "; + private Duration expiration = Duration.ofHours(1); + private Duration refreshExpiration = Duration.ofDays(7); + + // RSA 密钥配置(支持文件路径或直接内容) + private RsaKey rsa = new RsaKey(); + + // RSA 密钥内部类 + @Setter + @Getter + public static class RsaKey { + // Getter 和 Setter + /** + * 私钥路径或内容(用于签名) + * 优先级:content > location + */ + private String privateKey; + + /** + * 公钥路径或内容(用于验证) + * 优先级:content > location + */ + private String publicKey; + + /** + * 密钥算法,默认 RSA + */ + private String algorithm = "RSA"; + + /** + * 密钥长度,默认 2048 + */ + private int keySize = 2048; + + } + + // Getter 和 Setter(原有) + public String getHeader() { return header; } + public void setHeader(String header) { this.header = header; } + + public String getPrefix() { return prefix; } + public void setPrefix(String prefix) { this.prefix = prefix; } + + public Duration getExpiration() { return expiration; } + public void setExpiration(Duration expiration) { this.expiration = expiration; } + + public Duration getRefreshExpiration() { return refreshExpiration; } + public void setRefreshExpiration(Duration refreshExpiration) { this.refreshExpiration = refreshExpiration; } + + // RSA 配置 Getter 和 Setter + public RsaKey getRsa() { return rsa; } + public void setRsa(RsaKey rsa) { this.rsa = rsa; } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/KaptchaTextCreator.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/KaptchaTextCreator.java new file mode 100644 index 0000000..b334f43 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/KaptchaTextCreator.java @@ -0,0 +1,69 @@ +package org.lingniu.idp.config; + +import com.google.code.kaptcha.text.impl.DefaultTextCreator; + +import java.util.Random; + +/** + * 验证码文本生成器 + * + * @author portal + */ +public class KaptchaTextCreator extends DefaultTextCreator +{ + private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); + + @Override + public String getText() + { + Integer result = 0; + Random random = new Random(); + int x = random.nextInt(10); + int y = random.nextInt(10); + StringBuilder suChinese = new StringBuilder(); + int randomoperands = random.nextInt(3); + if (randomoperands == 0) + { + result = x * y; + suChinese.append(CNUMBERS[x]); + suChinese.append("*"); + suChinese.append(CNUMBERS[y]); + } + else if (randomoperands == 1) + { + if ((x != 0) && y % x == 0) + { + result = y / x; + suChinese.append(CNUMBERS[y]); + suChinese.append("/"); + suChinese.append(CNUMBERS[x]); + } + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + } + else + { + if (x >= y) + { + result = x - y; + suChinese.append(CNUMBERS[x]); + suChinese.append("-"); + suChinese.append(CNUMBERS[y]); + } + else + { + result = y - x; + suChinese.append(CNUMBERS[y]); + suChinese.append("-"); + suChinese.append(CNUMBERS[x]); + } + } + suChinese.append("=?@" + result); + return suChinese.toString(); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/OidcUserInfoMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/OidcUserInfoMapper.java new file mode 100644 index 0000000..7979926 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/OidcUserInfoMapper.java @@ -0,0 +1,106 @@ +package org.lingniu.idp.config; + +import org.lingniu.idp.enums.CustomScopes; +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.model.entity.SysUser; +import org.lingniu.idp.service.core.SysPermissionService; +import org.lingniu.idp.service.core.login.LoginService; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.function.Function; + +@Component +public class OidcUserInfoMapper + implements Function { + + private final LoginService loginService; + + + // @formatter:off + private static final List EMAIL_CLAIMS = Arrays.asList( + StandardClaimNames.EMAIL, + StandardClaimNames.EMAIL_VERIFIED + ); + private static final List PHONE_CLAIMS = Arrays.asList( + StandardClaimNames.PHONE_NUMBER, + StandardClaimNames.PHONE_NUMBER_VERIFIED + ); + private static final List PROFILE_CLAIMS = Arrays.asList( + StandardClaimNames.NAME, + StandardClaimNames.FAMILY_NAME, + StandardClaimNames.GIVEN_NAME, + StandardClaimNames.MIDDLE_NAME, + StandardClaimNames.NICKNAME, + StandardClaimNames.PREFERRED_USERNAME, + StandardClaimNames.PROFILE, + StandardClaimNames.PICTURE, + StandardClaimNames.WEBSITE, + StandardClaimNames.GENDER, + StandardClaimNames.BIRTHDATE, + StandardClaimNames.ZONEINFO, + StandardClaimNames.LOCALE, + StandardClaimNames.UPDATED_AT + ); + + public OidcUserInfoMapper(LoginService loginService) {this.loginService = loginService;} + // @formatter:on + + @Override + public OidcUserInfo apply(OidcUserInfoAuthenticationContext authenticationContext) { + OAuth2Authorization authorization = authenticationContext.getAuthorization(); + OidcIdToken idToken = authorization.getToken(OidcIdToken.class).getToken(); + OAuth2AccessToken accessToken = authenticationContext.getAccessToken(); + Map scopeRequestedClaims = getClaimsRequestedByScope(idToken.getClaims(), + accessToken.getScopes()); + String userName = authorization.getPrincipalName(); + SysUser sysUser = loginService.getUserDetail(userName); + scopeRequestedClaims.put("userId",sysUser.getUserId()); + scopeRequestedClaims.put("currentDeptId",sysUser.getDeptId()); + scopeRequestedClaims.put("username",sysUser.getUserName()); + scopeRequestedClaims.put("userDepts",sysUser.getDeptList()); + scopeRequestedClaims.put("userPosts",sysUser.getPosts()); + scopeRequestedClaims.put("nickName",sysUser.getNickName()); + scopeRequestedClaims.put("sex",sysUser.getSex()); + if(accessToken.getScopes().contains(CustomScopes.PERMS.name().toLowerCase())){ + Map permissionInfo = loginService.getPermissionInfo(sysUser); + scopeRequestedClaims.putAll(permissionInfo); + + } + return new OidcUserInfo(scopeRequestedClaims); + } + + private static Map getClaimsRequestedByScope(Map claims, + Set requestedScopes) { + Set scopeRequestedClaimNames = new HashSet<>(32); + scopeRequestedClaimNames.add(StandardClaimNames.SUB); + + if (requestedScopes.contains(OidcScopes.ADDRESS)) { + scopeRequestedClaimNames.add(StandardClaimNames.ADDRESS); + } + if (requestedScopes.contains(OidcScopes.EMAIL)) { + scopeRequestedClaimNames.addAll(EMAIL_CLAIMS); + } + if (requestedScopes.contains(OidcScopes.PHONE)) { + scopeRequestedClaimNames.addAll(PHONE_CLAIMS); + } + if (requestedScopes.contains(OidcScopes.PROFILE)) { + scopeRequestedClaimNames.addAll(PROFILE_CLAIMS); + } + + + + Map requestedClaims = new HashMap<>(claims); + requestedClaims.keySet().removeIf((claimName) -> !scopeRequestedClaimNames.contains(claimName)); + + return requestedClaims; + } + + } \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/ProjectConfig.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/ProjectConfig.java new file mode 100644 index 0000000..d58d949 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/ProjectConfig.java @@ -0,0 +1,85 @@ +package org.lingniu.idp.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 读取项目相关配置 + * + * @author q + */ +@Setter +@Getter +@Component +@ConfigurationProperties(prefix = "project") +public class ProjectConfig +{ + /** 项目名称 */ + private String name; + + /** 版本 */ + private String version; + + /** 版权年份 */ + private String copyrightYear; + + /** 上传路径 */ + @Getter + private static String profile; + + /** 获取地址开关 */ + @Getter + private static boolean addressEnabled; + + /** 验证码类型 */ + @Getter + private static String captchaType; + + public void setProfile(String profile) + { + ProjectConfig.profile = profile; + } + + public void setAddressEnabled(boolean addressEnabled) + { + ProjectConfig.addressEnabled = addressEnabled; + } + + public void setCaptchaType(String captchaType) { + ProjectConfig.captchaType = captchaType; + } + + /** + * 获取导入上传路径 + */ + public static String getImportPath() + { + return getProfile() + "/import"; + } + + /** + * 获取头像上传路径 + */ + public static String getAvatarPath() + { + return getProfile() + "/avatar"; + } + + /** + * 获取下载路径 + */ + public static String getDownloadPath() + { + return getProfile() + "/download/"; + } + + /** + * 获取上传路径 + */ + public static String getUploadPath() + { + return getProfile() + "/upload"; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/RedisConfig.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/RedisConfig.java new file mode 100644 index 0000000..b8cecc1 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/RedisConfig.java @@ -0,0 +1,70 @@ +package org.lingniu.idp.config; + +import org.lingniu.idp.config.serializer.FastJson2JsonRedisSerializer; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * redis配置 + * + * @author portal + */ +@Configuration +@EnableCaching +public class RedisConfig extends CachingConfigurerSupport +{ + @Bean + @SuppressWarnings(value = { "unchecked", "rawtypes" }) + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } + + @Bean + public DefaultRedisScript limitScript() + { + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(limitScriptText()); + redisScript.setResultType(Long.class); + return redisScript; + } + + /** + * 限流脚本 + */ + private String limitScriptText() + { + return "local key = KEYS[1]\n" + + "local count = tonumber(ARGV[1])\n" + + "local time = tonumber(ARGV[2])\n" + + "local current = redis.call('get', key);\n" + + "if current and tonumber(current) > count then\n" + + " return tonumber(current);\n" + + "end\n" + + "current = redis.call('incr', key)\n" + + "if tonumber(current) == 1 then\n" + + " redis.call('expire', key, time)\n" + + "end\n" + + "return tonumber(current);"; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/SecurityConfig.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/SecurityConfig.java new file mode 100644 index 0000000..11b9ce8 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/SecurityConfig.java @@ -0,0 +1,117 @@ +package org.lingniu.idp.config; + +import org.lingniu.idp.security.filter.login.MobilePasswordAuthenticationFilter; +import org.lingniu.idp.security.handler.AuthenticationEntryPointImpl; +import org.lingniu.idp.security.handler.LoginSuccessHandler; +import org.lingniu.idp.security.handler.LogoutSuccessHandlerImpl; +import org.lingniu.idp.security.provider.MobilePasswordAuthenticationProvider; +import org.lingniu.idp.security.provider.SmsCodeAuthenticationProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.Customizer; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.util.Arrays; +import java.util.List; + +@Configuration +//@EnableWebSecurity +public class SecurityConfig { + + + + + private final AuthenticationEntryPointImpl unauthorizedHandler; + private final LogoutSuccessHandlerImpl logoutSuccessHandler; + private final MobilePasswordAuthenticationProvider mobilePasswordAuthenticationProvider; + private final SmsCodeAuthenticationProvider smsCodeAuthenticationProvider; + private final LoginSuccessHandler successHandler; + + public SecurityConfig(AuthenticationEntryPointImpl unauthorizedHandler, LogoutSuccessHandlerImpl logoutSuccessHandler, MobilePasswordAuthenticationProvider mobilePasswordAuthenticationProvider, SmsCodeAuthenticationProvider smsCodeAuthenticationProvider, LoginSuccessHandler successHandler) { + this.unauthorizedHandler = unauthorizedHandler; + this.logoutSuccessHandler = logoutSuccessHandler; + this.mobilePasswordAuthenticationProvider = mobilePasswordAuthenticationProvider; + this.smsCodeAuthenticationProvider = smsCodeAuthenticationProvider; + this.successHandler = successHandler; + } + + @Bean + @Order(2) + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + // 禁用HTTP响应标头 + .headers((headersCustomizer) -> { + headersCustomizer.cacheControl(HeadersConfigurer.CacheControlConfig::disable).frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin); + }) + // 认证失败处理类 + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + // 基于token,所以不需要session + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers( + "/api/login/**", + "/captcha/image" + ).permitAll() + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) + // 添加Logout filter + .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)) + .rememberMe(remember -> remember + .tokenValiditySeconds(7 * 24 * 60 * 60) // 7天 + .key("remember-me-idp") + ); + addFilters(http); + + return http.build(); + } + private void addFilters(HttpSecurity http){ + MobilePasswordAuthenticationFilter mobilePasswordAuthenticationFilter = new MobilePasswordAuthenticationFilter(authenticationManager()); + mobilePasswordAuthenticationFilter.setAuthenticationSuccessHandler(successHandler); + http.addFilterBefore(mobilePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + } + /** + * 自定义JWT认证转换器 + */ + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); + grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + + return jwtAuthenticationConverter; + } + + @Bean + public AuthenticationManager authenticationManager() { + List providers = Arrays.asList( + mobilePasswordAuthenticationProvider, + smsCodeAuthenticationProvider + ); + + return new ProviderManager(providers); + } + + public static void main(String[] args) { + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + String portal = bCryptPasswordEncoder.encode("portal"); + System.out.println(portal); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/ThreadPoolConfig.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/ThreadPoolConfig.java new file mode 100644 index 0000000..0edf355 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/ThreadPoolConfig.java @@ -0,0 +1,64 @@ +package org.lingniu.idp.config; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.lingniu.idp.utils.Threads; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置 + * + * @author portal + **/ +@Configuration +public class ThreadPoolConfig +{ + // 核心线程池大小 + private int corePoolSize = 50; + + // 最大可创建的线程数 + private int maxPoolSize = 200; + + // 队列最大长度 + private int queueCapacity = 1000; + + // 线程池维护线程所允许的空闲时间 + private int keepAliveSeconds = 300; + + @Bean(name = "threadPoolTaskExecutor") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() + { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setMaxPoolSize(maxPoolSize); + executor.setCorePoolSize(corePoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setKeepAliveSeconds(keepAliveSeconds); + // 线程池对拒绝任务(无线程可用)的处理策略 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + return executor; + } + + /** + * 执行周期性或定时任务 + */ + @Bean(name = "scheduledExecutorService") + protected ScheduledExecutorService scheduledExecutorService() + { + return new ScheduledThreadPoolExecutor(corePoolSize, + new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy()) + { + @Override + protected void afterExecute(Runnable r, Throwable t) + { + super.afterExecute(r, t); + Threads.printException(r, t); + } + }; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/TomcatServerConfig.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/TomcatServerConfig.java new file mode 100644 index 0000000..bb7083c --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/TomcatServerConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020-2025 the original author or 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. + */ +package org.lingniu.idp.config; + +import org.apache.catalina.connector.Connector; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +/** + * @author Joe Grandja + * @since 1.3 + */ +@Profile("!test") // Exclude this from DemoAuthorizationServerApplicationTests and DemoAuthorizationServerConsentTests +@Configuration(proxyBeanMethods = false) +public class TomcatServerConfig { + + @Bean + public WebServerFactoryCustomizer connectorCustomizer() { + return (tomcat) -> tomcat.addAdditionalTomcatConnectors(createHttpConnector()); + } + + private Connector createHttpConnector() { + Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); + connector.setScheme("http"); + connector.setPort(8000); + connector.setSecure(false); + connector.setRedirectPort(8443); + return connector; + } + +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/serializer/FastJson2JsonRedisSerializer.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/serializer/FastJson2JsonRedisSerializer.java new file mode 100644 index 0000000..98e7e5d --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/serializer/FastJson2JsonRedisSerializer.java @@ -0,0 +1,53 @@ +package org.lingniu.idp.config.serializer; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.Filter; +import org.lingniu.idp.constant.Constants; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; + +import java.nio.charset.Charset; + +/** + * Redis使用FastJson序列化 + * + * @author portal + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer +{ + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR); + + private Class clazz; + + public FastJson2JsonRedisSerializer(Class clazz) + { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException + { + if (t == null) + { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException + { + if (bytes == null || bytes.length <= 0) + { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + + return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/serializer/SensitiveJsonSerializer.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/serializer/SensitiveJsonSerializer.java new file mode 100644 index 0000000..6ac0362 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/config/serializer/SensitiveJsonSerializer.java @@ -0,0 +1,68 @@ +package org.lingniu.idp.config.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import org.lingniu.idp.annotation.Sensitive; +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.enums.DesensitizedType; +import org.lingniu.idp.utils.SecurityUtils; + +import java.io.IOException; +import java.util.Objects; + +/** + * 数据脱敏序列化过滤 + * + * @author portal + */ +public class SensitiveJsonSerializer extends JsonSerializer implements ContextualSerializer +{ + private DesensitizedType desensitizedType; + + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException + { + if (desensitization()) + { + gen.writeString(desensitizedType.desensitizer().apply(value)); + } + else + { + gen.writeString(value); + } + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) + throws JsonMappingException + { + Sensitive annotation = property.getAnnotation(Sensitive.class); + if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) + { + this.desensitizedType = annotation.desensitizedType(); + return this; + } + return prov.findValueSerializer(property.getType(), property); + } + + /** + * 是否需要脱敏处理 + */ + private boolean desensitization() + { + try + { + LoginUser securityUser = SecurityUtils.getLoginUser(); + // 管理员不脱敏 + return !securityUser.getUser().isAdmin(); + } + catch (Exception e) + { + return true; + } + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/constant/CacheConstants.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/constant/CacheConstants.java new file mode 100644 index 0000000..07347f7 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/constant/CacheConstants.java @@ -0,0 +1,55 @@ +package org.lingniu.idp.constant; + +/** + * 缓存的key 常量 + * + */ +public class CacheConstants +{ + public static final String prefix = "idp_"; + /** + * 登录用户 redis key + */ + public static final String LOGIN_TOKEN_KEY = prefix + "login_tokens:"; + + /** + * 验证码 redis key + */ + public static final String CAPTCHA_CODE_KEY = prefix + "captcha_codes:"; + + /** + * 参数管理 cache key + */ + public static final String SYS_CONFIG_KEY = prefix + "sys_config:"; + + /** + * 字典管理 cache key + */ + public static final String SYS_DICT_KEY = prefix + "sys_dict:"; + + /** + * 防重提交 redis key + */ + public static final String REPEAT_SUBMIT_KEY = prefix + "repeat_submit:"; + + /** + * 限流 redis key + */ + public static final String RATE_LIMIT_KEY = prefix + "rate_limit:"; + + /** + * 登录账户密码错误次数 redis key + */ + public static final String PWD_ERR_CNT_KEY = prefix + "pwd_err_cnt:"; + + // Access Token存储: String结构 + // 格式: access_token:{token} + public static final String ACCESS_TOKEN_KEY = prefix + "access_token:%s"; + + // Refresh Token存储: Hash结构 + // 格式: refresh_token:{token} + public static final String REFRESH_TOKEN_KEY = prefix + "refresh_token:%s"; + + // 用户会话管理 + public static final String USER_SESSIONS = prefix + "user_sessions:%s"; // userId -> session列表 +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/constant/Constants.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/constant/Constants.java new file mode 100644 index 0000000..d1cc360 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/constant/Constants.java @@ -0,0 +1,176 @@ +package org.lingniu.idp.constant; + +import com.nimbusds.openid.connect.sdk.claims.ClaimType; +import com.nimbusds.openid.connect.sdk.claims.CommonClaimsSet; + +import java.util.Locale; + +/** + * 通用常量信息 + * + * @author portal + */ +public class Constants +{ + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * 系统语言 + */ + public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE; + + + /** + * www主域 + */ + public static final String WWW = "www."; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 通用成功标识 + */ + public static final String SUCCESS = "0"; + + /** + * 通用失败标识 + */ + public static final String FAIL = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 所有权限标识 + */ + public static final String ALL_PERMISSION = "*:*:*"; + + /** + * 管理员角色权限标识 + */ + public static final String SUPER_ADMIN = "admin"; + + /** + * 角色权限分隔符 + */ + public static final String ROLE_DELIMITER = ","; + + /** + * 权限标识分隔符 + */ + public static final String PERMISSION_DELIMITER = ","; + + /** + * 验证码有效期(分钟) + */ + public static final Integer CAPTCHA_EXPIRATION = 2; + + /** + * 令牌 + */ + public static final String TOKEN = "token"; + + /** + * 令牌前缀 + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * 令牌前缀 + */ + public static final String LOGIN_USER_KEY = "login_user_key"; + + /** + * 用户ID + */ + public static final String JWT_USERID = "userid"; + + /** + * 用户名称 + */ + public static final String JWT_USERNAME = CommonClaimsSet.SUB_CLAIM_NAME; + + /** + * 用户头像 + */ + public static final String JWT_AVATAR = "avatar"; + + /** + * 创建时间 + */ + public static final String JWT_CREATED = "created"; + + /** + * 用户权限 + */ + public static final String JWT_AUTHORITIES = "authorities"; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全) + */ + public static final String[] JSON_WHITELIST_STR = { "com.portal" }; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = { "com.portal.quartz.task" }; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "org.lingniu.idp.utils.file", "org.lingniu.idp.config", "com.portal.generator" }; +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/constant/UserConstants.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/constant/UserConstants.java new file mode 100644 index 0000000..895bdbf --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/constant/UserConstants.java @@ -0,0 +1,81 @@ +package org.lingniu.idp.constant; + +/** + * 用户常量信息 + * + * @author portal + */ +public class UserConstants +{ + /** + * 平台内系统用户的唯一标志 + */ + public static final String SYS_USER = "SYS_USER"; + + /** 正常状态 */ + public static final String NORMAL = "0"; + + /** 异常状态 */ + public static final String EXCEPTION = "1"; + + /** 用户封禁状态 */ + public static final String USER_DISABLE = "1"; + + /** 角色正常状态 */ + public static final String ROLE_NORMAL = "0"; + + /** 角色封禁状态 */ + public static final String ROLE_DISABLE = "1"; + + /** 部门正常状态 */ + public static final String DEPT_NORMAL = "0"; + + /** 部门停用状态 */ + public static final String DEPT_DISABLE = "1"; + + /** 字典正常状态 */ + public static final String DICT_NORMAL = "0"; + + /** 是否为系统默认(是) */ + public static final String YES = "Y"; + + /** 是否菜单外链(是) */ + public static final String YES_FRAME = "0"; + + /** 是否菜单外链(否) */ + public static final String NO_FRAME = "1"; + + /** 菜单类型(目录) */ + public static final String TYPE_DIR = "M"; + + /** 菜单类型(菜单) */ + public static final String TYPE_MENU = "C"; + + /** 菜单类型(按钮) */ + public static final String TYPE_BUTTON = "F"; + + /** Layout组件标识 */ + public final static String LAYOUT = "Layout"; + + /** ParentView组件标识 */ + public final static String PARENT_VIEW = "ParentView"; + + /** InnerLink组件标识 */ + public final static String INNER_LINK = "InnerLink"; + + /** 校验是否唯一的返回标识 */ + public final static boolean UNIQUE = true; + public final static boolean NOT_UNIQUE = false; + + /** + * 用户名长度限制 + */ + public static final int USERNAME_MIN_LENGTH = 2; + public static final int USERNAME_MAX_LENGTH = 20; + + /** + * 密码长度限制 + */ + public static final int PASSWORD_MIN_LENGTH = 5; + public static final int PASSWORD_MAX_LENGTH = 20; +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/controller/CaptchaController.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/controller/CaptchaController.java new file mode 100644 index 0000000..31bc6dd --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/controller/CaptchaController.java @@ -0,0 +1,97 @@ +package org.lingniu.idp.controller; + +import com.google.code.kaptcha.Producer; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.lingniu.idp.config.ProjectConfig; +import org.lingniu.idp.constant.CacheConstants; +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.model.base.AjaxResult; +import org.lingniu.idp.common.redis.RedisCache; +import org.lingniu.idp.utils.sign.Base64; +import org.lingniu.idp.utils.uuid.IdUtils; +import org.lingniu.idp.service.ISysConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.FastByteArrayOutputStream; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * 验证码操作处理 + * + * @author portal + */ +@RestController +@RequestMapping("/captcha") +public class CaptchaController +{ + @Resource(name = "captchaProducer") + private Producer captchaProducer; + + @Resource(name = "captchaProducerMath") + private Producer captchaProducerMath; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysConfigService configService; + /** + * 生成验证码 + */ + @GetMapping("/image") + public AjaxResult getCode(HttpServletResponse response) throws IOException + { + AjaxResult ajax = AjaxResult.success(); + boolean captchaEnabled = configService.selectCaptchaEnabled(); + ajax.put("captchaEnabled", captchaEnabled); + if (!captchaEnabled) + { + return ajax; + } + + // 保存验证码信息 + String uuid = IdUtils.simpleUUID(); + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; + + String capStr = null, code = null; + BufferedImage image = null; + + // 生成验证码 + String captchaType = ProjectConfig.getCaptchaType(); + if ("math".equals(captchaType)) + { + String capText = captchaProducerMath.createText(); + capStr = capText.substring(0, capText.lastIndexOf("@")); + code = capText.substring(capText.lastIndexOf("@") + 1); + image = captchaProducerMath.createImage(capStr); + } + else if ("char".equals(captchaType)) + { + capStr = code = captchaProducer.createText(); + image = captchaProducer.createImage(capStr); + } + + redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + // 转换流信息写出 + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + try + { + ImageIO.write(image, "jpg", os); + } + catch (IOException e) + { + return AjaxResult.error(e.getMessage()); + } + + ajax.put("uuid", uuid); + ajax.put("img", Base64.encode(os.toByteArray())); + return ajax; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/controller/SysLoginController.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/controller/SysLoginController.java new file mode 100644 index 0000000..1bc84d0 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/controller/SysLoginController.java @@ -0,0 +1,45 @@ +package org.lingniu.idp.controller; + +import org.lingniu.idp.model.base.AjaxResult; +import org.lingniu.idp.model.entity.SysMenu; +import org.lingniu.idp.model.entity.SysUser; +import org.lingniu.idp.service.ISysMenuService; +import org.lingniu.idp.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 登录验证 + * + * @author portal + */ +@RestController +@RequestMapping("/idp") +public class SysLoginController +{ + + @Autowired + private ISysUserService sysUserService; + @Autowired + private ISysMenuService menuService; + + + /** + * 获取路由信息 + * + * @return 路由信息 + */ + @GetMapping("getRouters") + public AjaxResult getRouters(@AuthenticationPrincipal Jwt jwt) + { + SysUser sysUser = sysUserService.selectUserByUserName(jwt.getSubject()); + List menus = menuService.selectMenuTreeByUserId(sysUser.getUserId()); + return AjaxResult.success(menuService.buildMenus(menus)); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/controller/login.http b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/controller/login.http new file mode 100644 index 0000000..46dd375 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/controller/login.http @@ -0,0 +1,48 @@ +### 验证码 +# @no-redirect +GET http://localhost:8000/captcha/image + +### 密码登录 054aae95108e409dadc12af52f556a70 +# @no-redirect +POST http://localhost:8000/api/login/account +Content-Type: application/x-www-form-urlencoded +Accept: application/json + +username=admin&password=admin123&uuid=1316459cfa7f40429bdc0f751229eb23&code=2 + + +##### e040b13e72e84086b63da369d60887e4 +POST http://localhost:8000/oauth2/authorize +Content-Type: application/x-www-form-urlencoded +Accept: application/json +Idp: 53c55ddb3caf44568ba429347fdad0e6 +Cookie: idp_refresh_token=7bb21a0dcac94aec99f08ae6a2d6db30 + +client_id=2c6f1d9ff78641c78d72a848&redirect_uri=http%3A%2F%2Flocalhost%3A81%2Fcallback&response_type=code&state=LXcbn1xobq6unvUCz5uwG7PcwtsNdbWg&scope=openid + +### @name 一次性授权码登录 +POST http://localhost:8000/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic MmM2ZjFkOWZmNzg2NDFjNzhkNzJhODQ4OmkxYnBlcjFKdzJnTGVUelVOMW9uaXd1SUNQRFFnTnVRRWNZeFRLSjVpdjA= + +grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A81%2Fcallback&code=qmkNPAOk7pKTrTcHecwfSuona-O9Kjs-bbgSthovqSZz-wBhrqh9SUVeVUEN8ct4Gr7V-Dt4xMeupw3gFslSlbxZ8t2UHl74-63rGp7xnDVUGFWAA99TVa8hOnSgABZX&state=LXcbn1xobq6unvUCz5uwG7PcwtsNdbWg + + +#### +GET http://localhost:8000/account/getInfo +Authorization: Bearer eyJraWQiOiJpZHAiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6IjJjNmYxZDlmZjc4NjQxYzc4ZDcyYTg0OCIsIm5iZiI6MTc3MDI1NTA3MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDAwIiwiZXhwIjoxNzcwMjU4NjcyLCJpYXQiOjE3NzAyNTUwNzIsImp0aSI6IjVmNzAxM2EwLTU0MjctNDMyOC04ZTg0LWUwZjE0YjA4Y2IyYSJ9.KbradWCC1p6mD-JEd3IefUuzNvpNyUvGwyfuTRBVCC7jh-QGU36j4WxkeCtNJqaCBWZWQZVCJj068ysqanbRZRiSA4nADFMZnRVdBJD340TknKcGp7PbmiJfPD_uh4OzLosAu9xEUPjEW6q6rrjEqtIA9brK8NfP6A8A7aB8fYyKE0V1VO6j06AwC1CmXxUGrDgtJpU9_4NhV1Jf4cLBtECVG0pQDWMrEUNtrShPoa8gNJUUhP0gU0g-PyqBwlRhVl7Ra6T5lr4IIlsgK2D0Zu0ssFSJjZv_zhTW5Lo3xbe_DKVbWV4buJvLrHzlGBdFn7IV1-x6FwSZX10FBvXtOA +### +GET http://localhost:8000/.well-known/openid-configuration + + +### +GET http://localhost:8000/userinfo +Authorization: Bearer eyJraWQiOiJpZHAiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6IjJjNmYxZDlmZjc4NjQxYzc4ZDcyYTg0OCIsIm5iZiI6MTc3MDM2NTc1Nywic2NvcGUiOlsib3BlbmlkIl0sImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImV4cCI6MTc3MDQwODk1NywiaWF0IjoxNzcwMzY1NzU3LCJqdGkiOiIwOWFjNDJjZi1jNmI4LTQ4MDMtOTkyMi0yMTYwMTk0Zjg1MzQifQ.TIrICdQ2iltZXS7PAGc1MHi_cHS9VuWgYsRZGI5t-ZoPjP1fvnkDg4OwO4In7vl1617-bUSlvl42Dm4AVZvtsnNWVVg6LXyf-Cge1kPENynnrpJzPRHkT5XwHno552UWwS6B3VtlSXsHnlK7D5BEZmsw4X5bL0fF56IPjoiaYjhgPoEp_Q9kU7b6d_2gnQAQeOst-K0yFtYtBIWl6QJX9q9Q-4maJdbuOAWqUOySTg2STKII3XYqK-r8sor4cv55itFqX4VZuhQuSQwbumLyJq55YYtl-vzYvST9zHRyeYxWaHVIhDZRNZR_AYkVB-Oj_8lASWRpDOSI10LJr4M1Qw + + +### +POST http://localhost:8000/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic MmM2ZjFkOWZmNzg2NDFjNzhkNzJhODQ4OmkxYnBlcjFKdzJnTGVUelVOMW9uaXd1SUNQRFFnTnVRRWNZeFRLSjVpdjA= + +grant_type=refresh_token&refresh_token=_NfU5Gdy_dANJkbvjJm6cK7PxNSHyQexWMY5KthA8Hs_nOFtPnTVChsHF-dmLjzhDRZk5nHNZWV7XhxyOp5qS-nLjScsrbvVwSmZhb20QpDLaSoUGtF-ZdBawvlceXks diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/BusinessStatus.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/BusinessStatus.java new file mode 100644 index 0000000..7eb0695 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/BusinessStatus.java @@ -0,0 +1,20 @@ +package org.lingniu.idp.enums; + +/** + * 操作状态 + * + * @author portal + * + */ +public enum BusinessStatus +{ + /** + * 成功 + */ + SUCCESS, + + /** + * 失败 + */ + FAIL, +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/BusinessType.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/BusinessType.java new file mode 100644 index 0000000..6815837 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/BusinessType.java @@ -0,0 +1,59 @@ +package org.lingniu.idp.enums; + +/** + * 业务操作类型 + * + * @author portal + */ +public enum BusinessType +{ + /** + * 其它 + */ + OTHER, + + /** + * 新增 + */ + INSERT, + + /** + * 修改 + */ + UPDATE, + + /** + * 删除 + */ + DELETE, + + /** + * 授权 + */ + GRANT, + + /** + * 导出 + */ + EXPORT, + + /** + * 导入 + */ + IMPORT, + + /** + * 强退 + */ + FORCE, + + /** + * 生成代码 + */ + GENCODE, + + /** + * 清空数据 + */ + CLEAN, +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/CustomScopes.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/CustomScopes.java new file mode 100644 index 0000000..ff3c202 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/CustomScopes.java @@ -0,0 +1,5 @@ +package org.lingniu.idp.enums; + +public enum CustomScopes { + PERMS +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DataScope.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DataScope.java new file mode 100644 index 0000000..7f6ceef --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DataScope.java @@ -0,0 +1,24 @@ +package org.lingniu.idp.enums; + +/** + * 1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限 + */ +public enum DataScope { + ALL(1), + CUSTOM(2), + DEPT_AND_SUB(3), + DEPT_SELF(4), + USER_SELF(5); + private Integer value; + DataScope(Integer value){ + this.value = value; + } + + public int value(){ + return value; + } + + public void setValue(Integer value) { + this.value = value; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DataSourceType.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DataSourceType.java new file mode 100644 index 0000000..b6066be --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DataSourceType.java @@ -0,0 +1,19 @@ +package org.lingniu.idp.enums; + +/** + * 数据源 + * + * @author portal + */ +public enum DataSourceType +{ + /** + * 主库 + */ + MASTER, + + /** + * 从库 + */ + SLAVE +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DesensitizedType.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DesensitizedType.java new file mode 100644 index 0000000..1085150 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DesensitizedType.java @@ -0,0 +1,60 @@ +package org.lingniu.idp.enums; + +import org.lingniu.idp.utils.DesensitizedUtil; + +import java.util.function.Function; + +/** + * 脱敏类型 + * + * @author portal + */ +public enum DesensitizedType +{ + /** + * 姓名,第2位星号替换 + */ + USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")), + + /** + * 密码,全部字符都用*代替 + */ + PASSWORD(DesensitizedUtil::password), + + /** + * 身份证,中间10位星号替换 + */ + ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\d{3}[Xx]|\\d{4})", "$1** **** ****$2")), + + /** + * 手机号,中间4位星号替换 + */ + PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")), + + /** + * 电子邮箱,仅显示第一个字母和@后面的地址显示,其他星号替换 + */ + EMAIL(s -> s.replaceAll("(^.)[^@]*(@.*$)", "$1****$2")), + + /** + * 银行卡号,保留最后4位,其他星号替换 + */ + BANK_CARD(s -> s.replaceAll("\\d{15}(\\d{3})", "**** **** **** **** $1")), + + /** + * 车牌号码,包含普通车辆、新能源车辆 + */ + CAR_LICENSE(DesensitizedUtil::carLicense); + + private final Function desensitizer; + + DesensitizedType(Function desensitizer) + { + this.desensitizer = desensitizer; + } + + public Function desensitizer() + { + return desensitizer; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DeviceType.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DeviceType.java new file mode 100644 index 0000000..f93c4c1 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/DeviceType.java @@ -0,0 +1,24 @@ +package org.lingniu.idp.enums; + + /** + * 设备类型枚举 + */ + public enum DeviceType { + WEB("网页"), + IOS("iOS"), + ANDROID("安卓"), + H5("H5"), + MINI_PROGRAM("小程序"), + DESKTOP("桌面端"), + OTHER("其他"); + + private final String description; + + DeviceType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/HttpMethod.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/HttpMethod.java new file mode 100644 index 0000000..6f4be23 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/HttpMethod.java @@ -0,0 +1,37 @@ +package org.lingniu.idp.enums; + +import org.springframework.lang.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * 请求方式 + * + * @author portal + */ +public enum HttpMethod +{ + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + + private static final Map mappings = new HashMap<>(16); + + static + { + for (HttpMethod httpMethod : values()) + { + mappings.put(httpMethod.name(), httpMethod); + } + } + + @Nullable + public static HttpMethod resolve(@Nullable String method) + { + return (method != null ? mappings.get(method) : null); + } + + public boolean matches(String method) + { + return (this == resolve(method)); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/LimitType.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/LimitType.java new file mode 100644 index 0000000..0ddc070 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/LimitType.java @@ -0,0 +1,20 @@ +package org.lingniu.idp.enums; + +/** + * 限流类型 + * + * @author portal + */ + +public enum LimitType +{ + /** + * 默认策略全局限流 + */ + DEFAULT, + + /** + * 根据请求者IP进行限流 + */ + IP +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/OperatorType.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/OperatorType.java new file mode 100644 index 0000000..9b6a2c4 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/OperatorType.java @@ -0,0 +1,24 @@ +package org.lingniu.idp.enums; + +/** + * 操作人类别 + * + * @author portal + */ +public enum OperatorType +{ + /** + * 其它 + */ + OTHER, + + /** + * 后台用户 + */ + MANAGE, + + /** + * 手机端用户 + */ + MOBILE +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/RevokeReason.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/RevokeReason.java new file mode 100644 index 0000000..a5217d0 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/RevokeReason.java @@ -0,0 +1,25 @@ +package org.lingniu.idp.enums; + +/** + * 撤销原因枚举 + */ + public enum RevokeReason { + USER_LOGOUT("用户主动登出"), + ADMIN_REVOKED("管理员撤销"), + DEVICE_CHANGED("设备变更"), + SUSPICIOUS_ACTIVITY("可疑活动"), + PASSWORD_CHANGED("密码修改"), + SESSION_EXPIRED("会话过期"), + SECURITY_POLICY("安全策略"), + OTHER("其他"); + + private final String description; + + RevokeReason(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/UserStatus.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/UserStatus.java new file mode 100644 index 0000000..e9b81ad --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/enums/UserStatus.java @@ -0,0 +1,30 @@ +package org.lingniu.idp.enums; + +/** + * 用户状态 + * + * @author portal + */ +public enum UserStatus +{ + OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除"); + + private final String code; + private final String info; + + UserStatus(String code, String info) + { + this.code = code; + this.info = info; + } + + public String getCode() + { + return code; + } + + public String getInfo() + { + return info; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/DemoModeException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/DemoModeException.java new file mode 100644 index 0000000..d28d3b7 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/DemoModeException.java @@ -0,0 +1,15 @@ +package org.lingniu.idp.exception; + +/** + * 演示模式异常 + * + * @author portal + */ +public class DemoModeException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public DemoModeException() + { + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/GlobalException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/GlobalException.java new file mode 100644 index 0000000..c99beb2 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/GlobalException.java @@ -0,0 +1,58 @@ +package org.lingniu.idp.exception; + +/** + * 全局异常 + * + * @author portal + */ +public class GlobalException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public GlobalException() + { + } + + public GlobalException(String message) + { + this.message = message; + } + + public String getDetailMessage() + { + return detailMessage; + } + + public GlobalException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } + + @Override + public String getMessage() + { + return message; + } + + public GlobalException setMessage(String message) + { + this.message = message; + return this; + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/ServiceException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/ServiceException.java new file mode 100644 index 0000000..49d5627 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/ServiceException.java @@ -0,0 +1,74 @@ +package org.lingniu.idp.exception; + +/** + * 业务异常 + * + * @author portal + */ +public final class ServiceException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private Integer code; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() + { + } + + public ServiceException(String message) + { + this.message = message; + } + + public ServiceException(String message, Integer code) + { + this.message = message; + this.code = code; + } + + public String getDetailMessage() + { + return detailMessage; + } + + @Override + public String getMessage() + { + return message; + } + + public Integer getCode() + { + return code; + } + + public ServiceException setMessage(String message) + { + this.message = message; + return this; + } + + public ServiceException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/UtilException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/UtilException.java new file mode 100644 index 0000000..d2500ea --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/UtilException.java @@ -0,0 +1,26 @@ +package org.lingniu.idp.exception; + +/** + * 工具类异常 + * + * @author portal + */ +public class UtilException extends RuntimeException +{ + private static final long serialVersionUID = 8247610319171014183L; + + public UtilException(Throwable e) + { + super(e.getMessage(), e); + } + + public UtilException(String message) + { + super(message); + } + + public UtilException(String message, Throwable throwable) + { + super(message, throwable); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/base/BaseException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/base/BaseException.java new file mode 100644 index 0000000..9e7334a --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/base/BaseException.java @@ -0,0 +1,97 @@ +package org.lingniu.idp.exception.base; + +import org.lingniu.idp.utils.MessageUtils; +import org.lingniu.idp.utils.StringUtils; + +/** + * 基础异常 + * + * @author portal + */ +public class BaseException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 所属模块 + */ + private String module; + + /** + * 错误码 + */ + private String code; + + /** + * 错误码对应的参数 + */ + private Object[] args; + + /** + * 错误消息 + */ + private String defaultMessage; + + public BaseException(String module, String code, Object[] args, String defaultMessage) + { + this.module = module; + this.code = code; + this.args = args; + this.defaultMessage = defaultMessage; + } + + public BaseException(String module, String code, Object[] args) + { + this(module, code, args, null); + } + + public BaseException(String module, String defaultMessage) + { + this(module, null, null, defaultMessage); + } + + public BaseException(String code, Object[] args) + { + this(null, code, args, null); + } + + public BaseException(String defaultMessage) + { + this(null, null, null, defaultMessage); + } + + @Override + public String getMessage() + { + String message = null; + if (!StringUtils.isEmpty(code)) + { + message = MessageUtils.message(code, args); + } + if (message == null) + { + message = defaultMessage; + } + return message; + } + + public String getModule() + { + return module; + } + + public String getCode() + { + return code; + } + + public Object[] getArgs() + { + return args; + } + + public String getDefaultMessage() + { + return defaultMessage; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/BlackListException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/BlackListException.java new file mode 100644 index 0000000..0ffe38a --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/BlackListException.java @@ -0,0 +1,18 @@ +package org.lingniu.idp.exception.user; + +import org.lingniu.idp.exception.user.UserException; + +/** + * 黑名单IP异常类 + * + * @author portal + */ +public class BlackListException extends UserException +{ + private static final long serialVersionUID = 1L; + + public BlackListException() + { + super("login.blocked", null); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/CaptchaException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/CaptchaException.java new file mode 100644 index 0000000..e880463 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/CaptchaException.java @@ -0,0 +1,18 @@ +package org.lingniu.idp.exception.user; + +import org.lingniu.idp.exception.user.UserException; + +/** + * 验证码错误异常类 + * + * @author portal + */ +public class CaptchaException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaException() + { + super("user.jcaptcha.error", null); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/CaptchaExpireException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/CaptchaExpireException.java new file mode 100644 index 0000000..8063624 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/CaptchaExpireException.java @@ -0,0 +1,18 @@ +package org.lingniu.idp.exception.user; + +import org.lingniu.idp.exception.user.UserException; + +/** + * 验证码失效异常类 + * + * @author portal + */ +public class CaptchaExpireException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaExpireException() + { + super("user.jcaptcha.expire", null); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserException.java new file mode 100644 index 0000000..c03d76c --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserException.java @@ -0,0 +1,18 @@ +package org.lingniu.idp.exception.user; + +import org.lingniu.idp.exception.base.BaseException; + +/** + * 用户信息异常类 + * + * @author portal + */ +public class UserException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public UserException(String code, Object[] args) + { + super("user", code, args, null); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserNotExistsException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserNotExistsException.java new file mode 100644 index 0000000..7b26368 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserNotExistsException.java @@ -0,0 +1,18 @@ +package org.lingniu.idp.exception.user; + +import org.lingniu.idp.exception.user.UserException; + +/** + * 用户不存在异常类 + * + * @author portal + */ +public class UserNotExistsException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserNotExistsException() + { + super("user.not.exists", null); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserPasswordNotMatchException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserPasswordNotMatchException.java new file mode 100644 index 0000000..51dc4d3 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserPasswordNotMatchException.java @@ -0,0 +1,18 @@ +package org.lingniu.idp.exception.user; + +import org.lingniu.idp.exception.user.UserException; + +/** + * 用户密码不正确或不符合规范异常类 + * + * @author portal + */ +public class UserPasswordNotMatchException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordNotMatchException() + { + super("user.password.not.match", null); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserPasswordRetryLimitExceedException.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserPasswordRetryLimitExceedException.java new file mode 100644 index 0000000..2ede09c --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/exception/user/UserPasswordRetryLimitExceedException.java @@ -0,0 +1,16 @@ +package org.lingniu.idp.exception.user; + +/** + * 用户错误最大次数异常类 + * + * @author portal + */ +public class UserPasswordRetryLimitExceedException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime) + { + super("user.password.retry.limit.exceed", new Object[] { retryLimitCount, lockTime }); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/manager/AsyncManager.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/manager/AsyncManager.java new file mode 100644 index 0000000..df94715 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/manager/AsyncManager.java @@ -0,0 +1,56 @@ +package org.lingniu.idp.manager; + +import org.lingniu.idp.utils.Threads; +import org.lingniu.idp.utils.spring.SpringUtils; + +import java.util.TimerTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 异步任务管理器 + * + * @author portal + */ +public class AsyncManager +{ + /** + * 操作延迟10毫秒 + */ + private final int OPERATE_DELAY_TIME = 10; + + /** + * 异步操作任务调度线程池 + */ + private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService"); + + /** + * 单例模式 + */ + private AsyncManager(){} + + private static AsyncManager me = new AsyncManager(); + + public static AsyncManager me() + { + return me; + } + + /** + * 执行任务 + * + * @param task 任务 + */ + public void execute(TimerTask task) + { + executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); + } + + /** + * 停止任务线程池 + */ + public void shutdown() + { + Threads.shutdownAndAwaitTermination(executor); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/manager/ShutdownManager.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/manager/ShutdownManager.java new file mode 100644 index 0000000..eb980de --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/manager/ShutdownManager.java @@ -0,0 +1,40 @@ +package org.lingniu.idp.manager; + +import jakarta.annotation.PreDestroy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + + +/** + * 确保应用退出时能关闭后台线程 + * + * @author portal + */ +@Component +public class ShutdownManager +{ + private static final Logger logger = LoggerFactory.getLogger("sys-user"); + + @PreDestroy + public void destroy() + { + shutdownAsyncManager(); + } + + /** + * 停止异步执行任务 + */ + private void shutdownAsyncManager() + { + try + { + logger.info("====关闭后台任务任务线程池===="); + AsyncManager.me().shutdown(); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/manager/factory/AsyncFactory.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/manager/factory/AsyncFactory.java new file mode 100644 index 0000000..3c4cacf --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/manager/factory/AsyncFactory.java @@ -0,0 +1,103 @@ +package org.lingniu.idp.manager.factory; +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.service.ISysLogininforService; +import org.lingniu.idp.service.ISysOperLogService; +import org.lingniu.idp.utils.LogUtils; +import org.lingniu.idp.utils.ServletUtils; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.utils.http.UserAgentUtils; +import org.lingniu.idp.utils.ip.AddressUtils; +import org.lingniu.idp.utils.ip.IpUtils; +import org.lingniu.idp.utils.spring.SpringUtils; + +import org.lingniu.idp.model.entity.SysLogininfor; +import org.lingniu.idp.model.entity.SysOperLog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.TimerTask; + +/** + * 异步工厂(产生任务用) + * + * @author portal + */ +public class AsyncFactory +{ + private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user"); + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息 + * @param args 列表 + * @return 任务task + */ + public static TimerTask recordLogininfor(final String username, final String status, final String message, + final Object... args) + { + final String userAgent = ServletUtils.getRequest().getHeader("User-Agent"); + final String ip = IpUtils.getIpAddr(); + return new TimerTask() + { + @Override + public void run() + { + String address = AddressUtils.getRealAddressByIP(ip); + StringBuilder s = new StringBuilder(); + s.append(LogUtils.getBlock(ip)); + s.append(address); + s.append(LogUtils.getBlock(username)); + s.append(LogUtils.getBlock(status)); + s.append(LogUtils.getBlock(message)); + // 打印信息到日志 + sys_user_logger.info(s.toString(), args); + // 获取客户端操作系统 + String os = UserAgentUtils.getOperatingSystem(userAgent); + // 获取客户端浏览器 + String browser = UserAgentUtils.getBrowser(userAgent); + // 封装对象 + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(ip); + logininfor.setLoginLocation(address); + logininfor.setBrowser(browser); + logininfor.setOs(os); + logininfor.setMsg(message); + // 日志状态 + if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) + { + logininfor.setStatus(Constants.SUCCESS); + } + else if (Constants.LOGIN_FAIL.equals(status)) + { + logininfor.setStatus(Constants.FAIL); + } + // 插入数据 + SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor); + } + }; + } + + /** + * 操作日志记录 + * + * @param operLog 操作日志信息 + * @return 任务task + */ + public static TimerTask recordOper(final SysOperLog operLog) + { + return new TimerTask() + { + @Override + public void run() + { + // 远程查询操作地点 + operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp())); + SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog); + } + }; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysConfigMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysConfigMapper.java new file mode 100644 index 0000000..3bd410e --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysConfigMapper.java @@ -0,0 +1,79 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.lingniu.idp.model.entity.SysConfig; + +/** + * 参数配置 数据层 + * + * @author portal + */ +@Mapper +public interface SysConfigMapper +{ + /** + * 查询参数配置信息 + * + * @param config 参数配置信息 + * @return 参数配置信息 + */ + public SysConfig selectConfig(SysConfig config); + + /** + * 通过ID查询配置 + * + * @param configId 参数ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数配置信息 + */ + public SysConfig checkConfigKeyUnique(String configKey); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 删除参数配置 + * + * @param configId 参数ID + * @return 结果 + */ + public int deleteConfigById(Long configId); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + public int deleteConfigByIds(Long[] configIds); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysDeptMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysDeptMapper.java new file mode 100644 index 0000000..d117079 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysDeptMapper.java @@ -0,0 +1,122 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.lingniu.idp.model.entity.SysDept; + +/** + * 部门管理 数据层 + * + * @author portal + */ +@Mapper +public interface SysDeptMapper +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @param deptCheckStrictly 部门树选择项是否关联显示 + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(@Param("roleId") Long roleId, @Param("deptCheckStrictly") boolean deptCheckStrictly); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + public List selectDeptListByUserRole(Long userId); + + /** + * 根据ID查询所有子部门 + * + * @param deptId 部门ID + * @return 部门列表 + */ + public List selectChildrenDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public int hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 + */ + public int checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param deptName 部门名称 + * @param parentId 父部门ID + * @return 结果 + */ + public SysDept checkDeptNameUnique(@Param("deptName") String deptName, @Param("parentId") Long parentId); + + /** + * 新增部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 修改所在部门正常状态 + * + * @param deptIds 部门ID组 + */ + public void updateDeptStatusNormal(Long[] deptIds); + + /** + * 修改子元素关系 + * + * @param depts 子元素 + * @return 结果 + */ + public int updateDeptChildren(@Param("depts") List depts); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysLogininforMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysLogininforMapper.java new file mode 100644 index 0000000..d97c0cb --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysLogininforMapper.java @@ -0,0 +1,45 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.lingniu.idp.model.entity.SysLogininfor; + +/** + * 系统访问日志情况信息 数据层 + * + * @author portal + */ +@Mapper +public interface SysLogininforMapper +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + * + * @return 结果 + */ + public int cleanLogininfor(); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysMenuMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysMenuMapper.java new file mode 100644 index 0000000..41501a4 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysMenuMapper.java @@ -0,0 +1,128 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.lingniu.idp.model.entity.SysMenu; + +/** + * 菜单表 数据层 + * + * @author portal + */ +@Mapper +public interface SysMenuMapper +{ + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu); + + /** + * 根据用户所有权限 + * + * @return 权限列表 + */ + public List selectMenuPerms(); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuListByUserId(SysMenu menu); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public List selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public List selectMenuPermsByUserId(Long userId); + + /** + * 根据用户ID查询菜单 + * + * @return 菜单列表 + */ + public List selectMenuTreeAll(); + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @param menuCheckStrictly 菜单树选择项是否关联显示 + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int hasChildByMenuId(Long menuId); + + /** + * 新增菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menuName 菜单名称 + * @param parentId 父菜单ID + * @return 结果 + */ + public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysOperLogMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysOperLogMapper.java new file mode 100644 index 0000000..40332b8 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysOperLogMapper.java @@ -0,0 +1,51 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.lingniu.idp.model.entity.SysOperLog; + +/** + * 操作日志 数据层 + * + * @author portal + */ +@Mapper +public interface SysOperLogMapper +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysPostMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysPostMapper.java new file mode 100644 index 0000000..b2f81b1 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysPostMapper.java @@ -0,0 +1,102 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.lingniu.idp.model.entity.SysPost; + +/** + * 岗位信息 数据层 + * + * @author portal + */ +@Mapper +public interface SysPostMapper +{ + /** + * 查询岗位数据集合 + * + * @param post 岗位信息 + * @return 岗位数据集合 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public List selectPostsByUserName(String userName); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 修改岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); + + /** + * 新增岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 校验岗位名称 + * + * @param postName 岗位名称 + * @return 结果 + */ + public SysPost checkPostNameUnique(String postName); + + /** + * 校验岗位编码 + * + * @param postCode 岗位编码 + * @return 结果 + */ + public SysPost checkPostCodeUnique(String postCode); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysRoleDeptMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysRoleDeptMapper.java new file mode 100644 index 0000000..08bda28 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysRoleDeptMapper.java @@ -0,0 +1,47 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.lingniu.idp.model.entity.SysRoleDept; + +/** + * 角色与部门关联表 数据层 + * + * @author portal + */ +@Mapper +public interface SysRoleDeptMapper +{ + /** + * 通过角色ID删除角色和部门关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleDeptByRoleId(Long roleId); + + /** + * 批量删除角色部门关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleDept(Long[] ids); + + /** + * 查询部门使用数量 + * + * @param deptId 部门ID + * @return 结果 + */ + public int selectCountRoleDeptByDeptId(Long deptId); + + /** + * 批量新增角色部门信息 + * + * @param roleDeptList 角色部门列表 + * @return 结果 + */ + public int batchRoleDept(List roleDeptList); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysRoleMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysRoleMapper.java new file mode 100644 index 0000000..cb146bd --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysRoleMapper.java @@ -0,0 +1,110 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.lingniu.idp.model.entity.SysRole; + +/** + * 角色表 数据层 + * + * @author portal + */ +@Mapper +public interface SysRoleMapper +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 根据用户ID查询角色 + * + * @param userName 用户名 + * @return 角色列表 + */ + public List selectRolesByUserName(String userName); + + /** + * 校验角色名称是否唯一 + * + * @param roleName 角色名称 + * @return 角色信息 + */ + public SysRole checkRoleNameUnique(String roleName); + + /** + * 校验角色权限是否唯一 + * + * @param roleKey 角色权限 + * @return 角色信息 + */ + public SysRole checkRoleKeyUnique(String roleKey); + + /** + * 修改角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 新增角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysRoleMenuMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..8e6de27 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysRoleMenuMapper.java @@ -0,0 +1,47 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.lingniu.idp.model.entity.SysRoleMenu; + +/** + * 角色与菜单关联表 数据层 + * + * @author portal + */ +@Mapper +public interface SysRoleMenuMapper +{ + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int checkMenuExistRole(Long menuId); + + /** + * 通过角色ID删除角色和菜单关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleMenuByRoleId(Long roleId); + + /** + * 批量删除角色菜单关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleMenu(Long[] ids); + + /** + * 批量新增角色菜单信息 + * + * @param roleMenuList 角色菜单列表 + * @return 结果 + */ + public int batchRoleMenu(List roleMenuList); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysUserMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysUserMapper.java new file mode 100644 index 0000000..f6f189b --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysUserMapper.java @@ -0,0 +1,150 @@ +package org.lingniu.idp.mapper; + +import java.util.Date; +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.lingniu.idp.model.entity.SysUser; + +/** + * 用户表 数据层 + * + * @author portal + */ +@Mapper +public interface SysUserMapper +{ + /** + * 根据条件分页查询用户列表 + * + * @param sysUser 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser sysUser); + + /** + * 根据条件分页查询已配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + public int updateUserAvatar(@Param("userId") Long userId, @Param("avatar") String avatar); + + /** + * 修改用户状态 + * + * @param userId 用户ID + * @param status 状态 + * @return 结果 + */ + public int updateUserStatus(@Param("userId") Long userId, @Param("status") String status); + + /** + * 更新用户登录信息(IP和登录时间) + * + * @param userId 用户ID + * @param loginIp 登录IP地址 + * @param loginDate 登录时间 + * @return 结果 + */ + public int updateLoginInfo(@Param("userId") Long userId, @Param("loginIp") String loginIp, @Param("loginDate") Date loginDate); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(@Param("userId") Long userId, @Param("password") String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public SysUser checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param phonenumber 手机号码 + * @return 结果 + */ + public SysUser checkPhoneUnique(String phonenumber); + + /** + * 校验email是否唯一 + * + * @param email 用户邮箱 + * @return 结果 + */ + public SysUser checkEmailUnique(String email); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysUserPostMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysUserPostMapper.java new file mode 100644 index 0000000..6b2f894 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysUserPostMapper.java @@ -0,0 +1,47 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.lingniu.idp.model.entity.SysUserPost; + +/** + * 用户与岗位关联表 数据层 + * + * @author portal + */ +@Mapper +public interface SysUserPostMapper +{ + /** + * 通过用户ID删除用户和岗位关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserPostByUserId(Long userId); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 批量删除用户和岗位关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserPost(Long[] ids); + + /** + * 批量新增用户岗位信息 + * + * @param userPostList 用户岗位列表 + * @return 结果 + */ + public int batchUserPost(List userPostList); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysUserRoleMapper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..e599c42 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/mapper/SysUserRoleMapper.java @@ -0,0 +1,65 @@ +package org.lingniu.idp.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.lingniu.idp.model.entity.SysUserRole; + +/** + * 用户与角色关联表 数据层 + * + * @author portal + */ +@Mapper +public interface SysUserRoleMapper +{ + /** + * 通过用户ID删除用户和角色关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserRoleByUserId(Long userId); + + /** + * 批量删除用户和角色关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserRole(Long[] ids); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 批量新增用户角色信息 + * + * @param userRoleList 用户角色列表 + * @return 结果 + */ + public int batchUserRole(List userRoleList); + + /** + * 删除用户和角色关联信息 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteUserRoleInfo(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int deleteUserRoleInfos(@Param("roleId") Long roleId, @Param("userIds") Long[] userIds); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/AjaxResult.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/AjaxResult.java new file mode 100644 index 0000000..0b47cd8 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/AjaxResult.java @@ -0,0 +1,187 @@ +package org.lingniu.idp.model.base; + +import org.lingniu.idp.utils.StringUtils; +import org.springframework.http.HttpStatus; + +import java.util.HashMap; +import java.util.Objects; + +/** + * 操作消息提醒 + * + * @author portal + */ +public class AjaxResult extends HashMap +{ + private static final long serialVersionUID = 1L; + + /** 状态码 */ + public static final String CODE_TAG = "code"; + + /** 返回内容 */ + public static final String MSG_TAG = "msg"; + + /** 数据对象 */ + public static final String DATA_TAG = "data"; + + /** + * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 + */ + public AjaxResult() + { + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + */ + public AjaxResult(int code, String msg) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + * @param data 数据对象 + */ + public AjaxResult(int code, String msg, Object data) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + if (StringUtils.isNotNull(data)) + { + super.put(DATA_TAG, data); + } + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static AjaxResult success() + { + return AjaxResult.success("操作成功"); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(Object data) + { + return AjaxResult.success("操作成功", data); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @return 成功消息 + */ + public static AjaxResult success(String msg) + { + return AjaxResult.success(msg, null); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 成功消息 + */ + public static AjaxResult success(String msg, Object data) + { + return new AjaxResult(HttpStatus.OK.value(), msg, data); + } + + + + /** + * 返回错误消息 + * + * @return 错误消息 + */ + public static AjaxResult error() + { + return AjaxResult.error("操作失败"); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(String msg) + { + return AjaxResult.error(msg, null); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 错误消息 + */ + public static AjaxResult error(String msg, Object data) + { + return new AjaxResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, data); + } + + /** + * 返回错误消息 + * + * @param code 状态码 + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(int code, String msg) + { + return new AjaxResult(code, msg, null); + } + + /** + * 是否为成功消息 + * + * @return 结果 + */ + public boolean isSuccess() + { + return Objects.equals(HttpStatus.OK.value(), this.get(CODE_TAG)); + } + + + /** + * 是否为错误消息 + * + * @return 结果 + */ + public boolean isError() + { + return Objects.equals(HttpStatus.INTERNAL_SERVER_ERROR.value(), this.get(CODE_TAG)); + } + + /** + * 方便链式调用 + * + * @param key 键 + * @param value 值 + * @return 数据对象 + */ + @Override + public AjaxResult put(String key, Object value) + { + super.put(key, value); + return this; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/BaseEntity.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/BaseEntity.java new file mode 100644 index 0000000..95a42db --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/BaseEntity.java @@ -0,0 +1,119 @@ +package org.lingniu.idp.model.base; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Entity基类 + * + * @author portal + */ +public class BaseEntity implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 搜索值 */ + @JsonIgnore + private String searchValue; + + /** 创建者 */ + private String createBy; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + private String updateBy; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + private String remark; + + /** 请求参数 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map params; + + public String getSearchValue() + { + return searchValue; + } + + public void setSearchValue(String searchValue) + { + this.searchValue = searchValue; + } + + public String getCreateBy() + { + return createBy; + } + + public void setCreateBy(String createBy) + { + this.createBy = createBy; + } + + public Date getCreateTime() + { + return createTime; + } + + public void setCreateTime(Date createTime) + { + this.createTime = createTime; + } + + public String getUpdateBy() + { + return updateBy; + } + + public void setUpdateBy(String updateBy) + { + this.updateBy = updateBy; + } + + public Date getUpdateTime() + { + return updateTime; + } + + public void setUpdateTime(Date updateTime) + { + this.updateTime = updateTime; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } + + public Map getParams() + { + if (params == null) + { + params = new HashMap<>(); + } + return params; + } + + public void setParams(Map params) + { + this.params = params; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/CommonResult.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/CommonResult.java new file mode 100644 index 0000000..4402af4 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/CommonResult.java @@ -0,0 +1,80 @@ +package org.lingniu.idp.model.base; + +import cn.hutool.core.lang.Assert; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.springframework.http.HttpStatus; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 错误码 + * + */ + private Integer code; + /** + * 错误提示,用户可阅读 + * + */ + private String msg; + /** + * 返回数据 + */ + private T data; + + /** + * 将传入的 result 对象,转换成另外一个泛型结果的对象 + * + * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 + * + * @param result 传入的 result 对象 + * @param 返回的泛型 + * @return 新的 CommonResult 对象 + */ + public static CommonResult error(CommonResult result) { + return error(result.getCode(), result.getMsg()); + } + + public static CommonResult error(Integer code, String message) { + Assert.notEquals(HttpStatus.OK.value(), code, "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + return result; + } + + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.code = HttpStatus.OK.value(); + result.data = data; + result.msg = ""; + return result; + } + + public static boolean isSuccess(Integer code) { + return Objects.equals(code, HttpStatus.OK.value()); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isSuccess() { + return isSuccess(code); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isError() { + return !isSuccess(); + } + +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/TreeSelect.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/TreeSelect.java new file mode 100644 index 0000000..74a4c4e --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/base/TreeSelect.java @@ -0,0 +1,94 @@ +package org.lingniu.idp.model.base; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.lingniu.idp.constant.UserConstants; +import org.lingniu.idp.model.entity.SysDept; +import org.lingniu.idp.model.entity.SysMenu; +import org.lingniu.idp.utils.StringUtils; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Treeselect树结构实体类 + * + * @author portal + */ +public class TreeSelect implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 节点ID */ + private Long id; + + /** 节点名称 */ + private String label; + + /** 节点禁用 */ + private boolean disabled = false; + + /** 子节点 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect() + { + + } + + public TreeSelect(SysDept dept) + { + this.id = dept.getDeptId(); + this.label = dept.getDeptName(); + this.disabled = StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()); + this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public TreeSelect(SysMenu menu) + { + this.id = menu.getMenuId(); + this.label = menu.getMenuName(); + this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public boolean isDisabled() + { + return disabled; + } + + public void setDisabled(boolean disabled) + { + this.disabled = disabled; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/dto/AccountLoginDto.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/dto/AccountLoginDto.java new file mode 100644 index 0000000..4edaf8d --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/dto/AccountLoginDto.java @@ -0,0 +1,35 @@ +package org.lingniu.idp.model.dto; + +import lombok.Getter; +import lombok.Setter; + +/** + * 用户登录对象 + * + * @author portal + */ +@Setter +@Getter +public class AccountLoginDto +{ + /** + * 用户名 + */ + private String username; + + /** + * 用户密码 + */ + private String password; + + /** + * 验证码 + */ + private String code; + + /** + * 唯一标识 + */ + private String uuid; + +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/dto/LoginUser.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/dto/LoginUser.java new file mode 100644 index 0000000..efd3236 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/dto/LoginUser.java @@ -0,0 +1,169 @@ +package org.lingniu.idp.model.dto; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Getter; +import lombok.Setter; +import org.lingniu.idp.model.entity.SysUser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.io.Serial; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +/** + * 登录用户身份权限 + * + * @author portal + */ +@Setter +@Getter +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonIgnoreProperties(ignoreUnknown = true) +public class LoginUser implements UserDetails +{ + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地点 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 用户信息 + */ + private SysUser user; + + public LoginUser() + { + } + + public LoginUser(SysUser user, Set permissions) + { + this.user = user; + this.permissions = permissions; + } + + public LoginUser(Long userId, Long deptId, SysUser user, Set permissions) + { + this.userId = userId; + this.deptId = deptId; + this.user = user; + this.permissions = permissions; + } + + @JSONField(serialize = false) + @Override + public String getPassword() + { + return user.getPassword(); + } + + @Override + public String getUsername() + { + return user.getUserName(); + } + + /** + * 账户是否未过期,过期无法验证 + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonExpired() + { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonLocked() + { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isCredentialsNonExpired() + { + return true; + } + + /** + * 是否可用 ,禁用的用户不能身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isEnabled() + { + return true; + } + + @Override + public Collection getAuthorities() + { + return null; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/dto/RegisterAccountDto.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/dto/RegisterAccountDto.java new file mode 100644 index 0000000..ac77212 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/dto/RegisterAccountDto.java @@ -0,0 +1,11 @@ +package org.lingniu.idp.model.dto; + +/** + * 用户注册对象 + * + * @author portal + */ +public class RegisterAccountDto extends AccountLoginDto +{ + +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysCache.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysCache.java new file mode 100644 index 0000000..da2dfeb --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysCache.java @@ -0,0 +1,81 @@ +package org.lingniu.idp.model.entity; + +import org.lingniu.idp.utils.StringUtils; + +/** + * 缓存信息 + * + * @author portal + */ +public class SysCache +{ + /** 缓存名称 */ + private String cacheName = ""; + + /** 缓存键名 */ + private String cacheKey = ""; + + /** 缓存内容 */ + private String cacheValue = ""; + + /** 备注 */ + private String remark = ""; + + public SysCache() + { + + } + + public SysCache(String cacheName, String remark) + { + this.cacheName = cacheName; + this.remark = remark; + } + + public SysCache(String cacheName, String cacheKey, String cacheValue) + { + this.cacheName = StringUtils.replace(cacheName, ":", ""); + this.cacheKey = StringUtils.replace(cacheKey, cacheName, ""); + this.cacheValue = cacheValue; + } + + public String getCacheName() + { + return cacheName; + } + + public void setCacheName(String cacheName) + { + this.cacheName = cacheName; + } + + public String getCacheKey() + { + return cacheKey; + } + + public void setCacheKey(String cacheKey) + { + this.cacheKey = cacheKey; + } + + public String getCacheValue() + { + return cacheValue; + } + + public void setCacheValue(String cacheValue) + { + this.cacheValue = cacheValue; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysConfig.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysConfig.java new file mode 100644 index 0000000..c6a7361 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysConfig.java @@ -0,0 +1,104 @@ +package org.lingniu.idp.model.entity; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.lingniu.idp.model.base.BaseEntity; + +/** + * 参数配置表 sys_config + * + * @author portal + */ +public class SysConfig extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 参数主键 */ + private Long configId; + + /** 参数名称 */ + private String configName; + + /** 参数键名 */ + private String configKey; + + /** 参数键值 */ + private String configValue; + + /** 系统内置(Y是 N否) */ + private String configType; + + public Long getConfigId() + { + return configId; + } + + public void setConfigId(Long configId) + { + this.configId = configId; + } + + @NotBlank(message = "参数名称不能为空") + @Size(min = 0, max = 100, message = "参数名称不能超过100个字符") + public String getConfigName() + { + return configName; + } + + public void setConfigName(String configName) + { + this.configName = configName; + } + + @NotBlank(message = "参数键名长度不能为空") + @Size(min = 0, max = 100, message = "参数键名长度不能超过100个字符") + public String getConfigKey() + { + return configKey; + } + + public void setConfigKey(String configKey) + { + this.configKey = configKey; + } + + @NotBlank(message = "参数键值不能为空") + @Size(min = 0, max = 500, message = "参数键值长度不能超过500个字符") + public String getConfigValue() + { + return configValue; + } + + public void setConfigValue(String configValue) + { + this.configValue = configValue; + } + + public String getConfigType() + { + return configType; + } + + public void setConfigType(String configType) + { + this.configType = configType; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("configId", getConfigId()) + .append("configName", getConfigName()) + .append("configKey", getConfigKey()) + .append("configValue", getConfigValue()) + .append("configType", getConfigType()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysDept.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysDept.java new file mode 100644 index 0000000..d129cac --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysDept.java @@ -0,0 +1,102 @@ +package org.lingniu.idp.model.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import jakarta.validation.constraints.Email; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.lingniu.idp.model.base.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.io.Serial; +import java.util.ArrayList; +import java.util.List; + +/** + * 部门表 sys_dept + * + * @author portal + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonIgnoreProperties(ignoreUnknown = true) +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SysDept extends BaseEntity +{ + @Serial + private static final long serialVersionUID = 1L; + + /** 部门ID */ + private Long deptId; + + /** 父部门ID */ + private Long parentId; + + /** 祖级列表 */ + private String ancestors; + + /** 部门名称 */ + @NotBlank(message = "部门名称不能为空") + @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符") + private String deptName; + + /** 显示顺序 */ + @NotNull(message = "显示顺序不能为空") + private Integer orderNum; + + /** 负责人 */ + private String leader; + + /** 联系电话 */ + @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符") + private String phone; + + /** 邮箱 */ + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + private String email; + + /** 部门状态:0正常,1停用 */ + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 父部门名称 */ + private String parentName; + /**运维区域*/ + private Long areaId; + + /** 子部门 */ + private List children = new ArrayList(); + + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("deptId", getDeptId()) + .append("parentId", getParentId()) + .append("ancestors", getAncestors()) + .append("deptName", getDeptName()) + .append("orderNum", getOrderNum()) + .append("leader", getLeader()) + .append("phone", getPhone()) + .append("email", getEmail()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysLogininfor.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysLogininfor.java new file mode 100644 index 0000000..58889b8 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysLogininfor.java @@ -0,0 +1,134 @@ +package org.lingniu.idp.model.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import org.lingniu.idp.model.base.BaseEntity; + +import java.util.Date; + +/** + * 系统访问记录表 sys_logininfor + * + * @author portal + */ +public class SysLogininfor extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Long infoId; + + /** 用户账号 */ + private String userName; + + /** 登录状态 0成功 1失败 */ + private String status; + + /** 登录IP地址 */ + private String ipaddr; + + /** 登录地点 */ + private String loginLocation; + + /** 浏览器类型 */ + private String browser; + + /** 操作系统 */ + private String os; + + /** 提示消息 */ + private String msg; + + /** 访问时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date loginTime; + + public Long getInfoId() + { + return infoId; + } + + public void setInfoId(Long infoId) + { + this.infoId = infoId; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public Date getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Date loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysMenu.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysMenu.java new file mode 100644 index 0000000..4d30f8b --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysMenu.java @@ -0,0 +1,278 @@ +package org.lingniu.idp.model.entity; + +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.NotBlank; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.lingniu.idp.model.base.BaseEntity; + + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.ArrayList; +import java.util.List; + +/** + * 菜单权限表 sys_menu + * + * @author portal + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SysMenu extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 菜单ID */ + private Long menuId; + + /** 菜单名称 */ + private String menuName; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 路由地址 */ + private String path; + + /** 组件路径 */ + private String component; + + /** 路由参数 */ + private String query; + + /** 路由名称,默认和路由地址相同的驼峰格式(注意:因为vue3版本的router会删除名称相同路由,为避免名字的冲突,特殊情况可以自定义) */ + private String routeName; + + /** 是否为外链(0是 1否) */ + private String isFrame; + + /** 是否缓存(0缓存 1不缓存) */ + private String isCache; + + /** 类型(M目录 C菜单 F按钮) */ + private String menuType; + + /** 显示状态(0显示 1隐藏) */ + private String visible; + + /** 菜单状态(0正常 1停用) */ + private String status; + + /** 权限字符串 */ + private String perms; + + /** 菜单图标 */ + private String icon; + + /** 子菜单 */ + private List children = new ArrayList(); + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @NotBlank(message = "菜单名称不能为空") + @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符") + public String getMenuName() + { + return menuName; + } + + public void setMenuName(String menuName) + { + this.menuName = menuName; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + @Size(min = 0, max = 200, message = "路由地址不能超过200个字符") + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + @Size(min = 0, max = 200, message = "组件路径不能超过255个字符") + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public String getRouteName() + { + return routeName; + } + + public void setRouteName(String routeName) + { + this.routeName = routeName; + } + + public String getIsFrame() + { + return isFrame; + } + + public void setIsFrame(String isFrame) + { + this.isFrame = isFrame; + } + + public String getIsCache() + { + return isCache; + } + + public void setIsCache(String isCache) + { + this.isCache = isCache; + } + + @NotBlank(message = "菜单类型不能为空") + public String getMenuType() + { + return menuType; + } + + public void setMenuType(String menuType) + { + this.menuType = menuType; + } + + public String getVisible() + { + return visible; + } + + public void setVisible(String visible) + { + this.visible = visible; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符") + public String getPerms() + { + return perms; + } + + public void setPerms(String perms) + { + this.perms = perms; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("menuId", getMenuId()) + .append("menuName", getMenuName()) + .append("parentId", getParentId()) + .append("orderNum", getOrderNum()) + .append("path", getPath()) + .append("component", getComponent()) + .append("query", getQuery()) + .append("routeName", getRouteName()) + .append("isFrame", getIsFrame()) + .append("IsCache", getIsCache()) + .append("menuType", getMenuType()) + .append("visible", getVisible()) + .append("status ", getStatus()) + .append("perms", getPerms()) + .append("icon", getIcon()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysOperLog.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysOperLog.java new file mode 100644 index 0000000..14d47a9 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysOperLog.java @@ -0,0 +1,251 @@ +package org.lingniu.idp.model.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import org.lingniu.idp.model.base.BaseEntity; + +import java.util.Date; + +/** + * 操作日志记录表 oper_log + * + * @author portal + */ +public class SysOperLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 日志主键 */ + private Long operId; + + /** 操作模块 */ + private String title; + + /** 业务类型(0其它 1新增 2修改 3删除) */ + private Integer businessType; + + /** 业务类型数组 */ + private Integer[] businessTypes; + + /** 请求方法 */ + private String method; + + /** 请求方式 */ + private String requestMethod; + + /** 操作类别(0其它 1后台用户 2手机端用户) */ + private Integer operatorType; + + /** 操作人员 */ + private String operName; + + /** 部门名称 */ + private String deptName; + + /** 请求url */ + private String operUrl; + + /** 操作地址 */ + private String operIp; + + /** 操作地点 */ + private String operLocation; + + /** 请求参数 */ + private String operParam; + + /** 返回参数 */ + private String jsonResult; + + /** 操作状态(0正常 1异常) */ + private Integer status; + + /** 错误消息 */ + private String errorMsg; + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date operTime; + + /** 消耗时间 */ + private Long costTime; + + public Long getOperId() + { + return operId; + } + + public void setOperId(Long operId) + { + this.operId = operId; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public Integer getBusinessType() + { + return businessType; + } + + public void setBusinessType(Integer businessType) + { + this.businessType = businessType; + } + + public Integer[] getBusinessTypes() + { + return businessTypes; + } + + public void setBusinessTypes(Integer[] businessTypes) + { + this.businessTypes = businessTypes; + } + + public String getMethod() + { + return method; + } + + public void setMethod(String method) + { + this.method = method; + } + + public String getRequestMethod() + { + return requestMethod; + } + + public void setRequestMethod(String requestMethod) + { + this.requestMethod = requestMethod; + } + + public Integer getOperatorType() + { + return operatorType; + } + + public void setOperatorType(Integer operatorType) + { + this.operatorType = operatorType; + } + + public String getOperName() + { + return operName; + } + + public void setOperName(String operName) + { + this.operName = operName; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getOperUrl() + { + return operUrl; + } + + public void setOperUrl(String operUrl) + { + this.operUrl = operUrl; + } + + public String getOperIp() + { + return operIp; + } + + public void setOperIp(String operIp) + { + this.operIp = operIp; + } + + public String getOperLocation() + { + return operLocation; + } + + public void setOperLocation(String operLocation) + { + this.operLocation = operLocation; + } + + public String getOperParam() + { + return operParam; + } + + public void setOperParam(String operParam) + { + this.operParam = operParam; + } + + public String getJsonResult() + { + return jsonResult; + } + + public void setJsonResult(String jsonResult) + { + this.jsonResult = jsonResult; + } + + public Integer getStatus() + { + return status; + } + + public void setStatus(Integer status) + { + this.status = status; + } + + public String getErrorMsg() + { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) + { + this.errorMsg = errorMsg; + } + + public Date getOperTime() + { + return operTime; + } + + public void setOperTime(Date operTime) + { + this.operTime = operTime; + } + + public Long getCostTime() + { + return costTime; + } + + public void setCostTime(Long costTime) + { + this.costTime = costTime; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysPost.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysPost.java new file mode 100644 index 0000000..771efbb --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysPost.java @@ -0,0 +1,72 @@ +package org.lingniu.idp.model.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.lingniu.idp.model.base.BaseEntity; + +import java.io.Serial; + +/** + * 岗位表 sys_post + * + * @author portal + */ +@Getter +@Setter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SysPost extends BaseEntity +{ + + @Serial + private static final long serialVersionUID = 1L; + + /** 岗位序号 */ + private Long postId; + + /** 岗位编码 */ + @NotBlank(message = "岗位编码不能为空") + @Size(min = 0, max = 64, message = "岗位编码长度不能超过64个字符") + private String postCode; + + /** 岗位名称 */ + @NotBlank(message = "岗位名称不能为空") + @Size(min = 0, max = 50, message = "岗位名称长度不能超过50个字符") + private String postName; + + /** 岗位排序 */ + @NotNull(message = "显示顺序不能为空") + private Integer postSort; + + /** 状态(0正常 1停用) */ + private String status; + + /** 用户是否存在此岗位标识 默认不存在 */ + private boolean flag = false; + + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("postId", getPostId()) + .append("postCode", getPostCode()) + .append("postName", getPostName()) + .append("postSort", getPostSort()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysRole.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysRole.java new file mode 100644 index 0000000..95a7579 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysRole.java @@ -0,0 +1,111 @@ +package org.lingniu.idp.model.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.lingniu.idp.model.base.BaseEntity; + +import java.io.Serial; +import java.util.Set; + +/** + * 角色表 sys_role + * + * @author portal + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonIgnoreProperties(ignoreUnknown = true) +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SysRole extends BaseEntity +{ + + @Serial + private static final long serialVersionUID = 1L; + + /** 角色ID */ + private Long roleId; + + /** 角色名称 */ + @NotBlank(message = "角色名称不能为空") + @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符") + private String roleName; + + /** 角色权限 */ + @NotBlank(message = "权限字符不能为空") + @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符") + private String roleKey; + + /** 角色排序 */ + @NotNull(message = "显示顺序不能为空") + private Integer roleSort; + + /** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) */ + private String dataScope; + + /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */ + private boolean menuCheckStrictly; + + /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */ + private boolean deptCheckStrictly; + + /** 角色状态(0正常 1停用) */ + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 用户是否存在此角色标识 默认不存在 */ + private boolean flag = false; + + /** 菜单组 */ + private Long[] menuIds; + + /** 部门组(数据权限) */ + private Long[] deptIds; + + /** 角色菜单权限 */ + private Set permissions; + public SysRole(Long roleId){ + this.roleId = roleId; + } + public boolean isAdmin() + { + return isAdmin(this.roleId); + } + + public static boolean isAdmin(Long roleId) + { + return roleId != null && 1L == roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("roleName", getRoleName()) + .append("roleKey", getRoleKey()) + .append("roleSort", getRoleSort()) + .append("dataScope", getDataScope()) + .append("menuCheckStrictly", isMenuCheckStrictly()) + .append("deptCheckStrictly", isDeptCheckStrictly()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysRoleDept.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysRoleDept.java new file mode 100644 index 0000000..cff8ac2 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysRoleDept.java @@ -0,0 +1,46 @@ +package org.lingniu.idp.model.entity; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和部门关联 sys_role_dept + * + * @author portal + */ +public class SysRoleDept +{ + /** 角色ID */ + private Long roleId; + + /** 部门ID */ + private Long deptId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("deptId", getDeptId()) + .toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysRoleMenu.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysRoleMenu.java new file mode 100644 index 0000000..8f86d97 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysRoleMenu.java @@ -0,0 +1,46 @@ +package org.lingniu.idp.model.entity; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和菜单关联 sys_role_menu + * + * @author portal + */ +public class SysRoleMenu +{ + /** 角色ID */ + private Long roleId; + + /** 菜单ID */ + private Long menuId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("menuId", getMenuId()) + .toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUser.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUser.java new file mode 100644 index 0000000..5717ec1 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUser.java @@ -0,0 +1,141 @@ +package org.lingniu.idp.model.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.lingniu.idp.common.xss.Xss; +import org.lingniu.idp.model.base.BaseEntity; + +import java.io.Serial; +import java.util.Date; +import java.util.List; + +/** + * 用户对象 sys_user + * + * @author portal + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonIgnoreProperties(ignoreUnknown = true) +@NoArgsConstructor +@Getter +@Setter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SysUser extends BaseEntity +{ + @Serial + private static final long serialVersionUID = 1L; + + /** 用户ID */ + private Long userId; + + /** 部门ID */ + private Long deptId; + + /** 用户账号 */ + @Xss(message = "用户账号不能包含脚本字符") + @NotBlank(message = "用户账号不能为空") + @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") + private String userName; + + /** 用户昵称 */ + @Xss(message = "用户昵称不能包含脚本字符") + @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") + private String nickName; + + /** 用户邮箱 */ + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + private String email; + + /** 手机号码 */ + @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符") + private String phonenumber; + + /** 用户性别 */ + private String sex; + + /** 用户头像 */ + private String avatar; + + /** 密码 */ + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String password; + + /** 账号状态(0正常 1停用) */ + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 最后登录IP */ + private String loginIp; + + /** 最后登录时间 */ + private Date loginDate; + + /** 密码最后更新时间 */ + private Date pwdUpdateDate; + + private SysDept dept; + private List deptList; + private List posts; + + /** 角色对象 */ + private List roles; + + /** 角色组 */ + private Long[] roleIds; + + /** 岗位组 */ + private Long[] postIds; + + /** 角色ID */ + private Long roleId; + public SysUser(Long userId){ + this.userId = userId; + } + + public boolean isAdmin(){ + return userId!=null && userId==1; + } + public static boolean isAdmin(Long userId){ + return userId!=null && userId==1; + } + + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("deptId", getDeptId()) + .append("userName", getUserName()) + .append("nickName", getNickName()) + .append("email", getEmail()) + .append("phonenumber", getPhonenumber()) + .append("sex", getSex()) + .append("avatar", getAvatar()) + .append("password", getPassword()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("loginIp", getLoginIp()) + .append("loginDate", getLoginDate()) + .append("pwdUpdateDate", getPwdUpdateDate()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .append("dept", getDept()) + .toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUserOnline.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUserOnline.java new file mode 100644 index 0000000..90a1652 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUserOnline.java @@ -0,0 +1,113 @@ +package org.lingniu.idp.model.entity; + +/** + * 当前在线会话 + * + * @author portal + */ +public class SysUserOnline +{ + /** 会话编号 */ + private String tokenId; + + /** 部门名称 */ + private String deptName; + + /** 用户名称 */ + private String userName; + + /** 登录IP地址 */ + private String ipaddr; + + /** 登录地址 */ + private String loginLocation; + + /** 浏览器类型 */ + private String browser; + + /** 操作系统 */ + private String os; + + /** 登录时间 */ + private Long loginTime; + + public String getTokenId() + { + return tokenId; + } + + public void setTokenId(String tokenId) + { + this.tokenId = tokenId; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUserPost.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUserPost.java new file mode 100644 index 0000000..523272e --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUserPost.java @@ -0,0 +1,46 @@ +package org.lingniu.idp.model.entity; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和岗位关联 sys_user_post + * + * @author portal + */ +public class SysUserPost +{ + /** 用户ID */ + private Long userId; + + /** 岗位ID */ + private Long postId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("postId", getPostId()) + .toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUserRole.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUserRole.java new file mode 100644 index 0000000..4517a73 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/entity/SysUserRole.java @@ -0,0 +1,46 @@ +package org.lingniu.idp.model.entity; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和角色关联 sys_user_role + * + * @author portal + */ +public class SysUserRole +{ + /** 用户ID */ + private Long userId; + + /** 角色ID */ + private Long roleId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("roleId", getRoleId()) + .toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/AccessTokenInfo.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/AccessTokenInfo.java new file mode 100644 index 0000000..a8d2865 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/AccessTokenInfo.java @@ -0,0 +1,246 @@ +package org.lingniu.idp.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.lingniu.idp.enums.DeviceType; + +import java.time.Instant; +import java.util.*; + +/** + * Access Token 信息 + * 存储在 Redis String 结构中 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AccessTokenInfo { + private String tokenValue; + + /** + * 用户ID + */ + private String userId; + + /** + * 用户名 + */ + private String username; + + /** + * 客户端ID + */ + private String clientId; + + /** + * 权限范围 + */ + private Set scopes; + + /** + * 颁发时间 + */ + private Instant issuedAt; + + /** + * 过期时间 + */ + private Instant expiresAt; + + /** + * 设备ID(可选,用于设备管理) + */ + private String deviceId; + + /** + * 设备类型(WEB/IOS/ANDROID) + */ + private DeviceType deviceType; + + /** + * IP地址 + */ + private String ipAddress; + + /** + * 用户代理(User-Agent) + */ + private String userAgent; + + /** + * 是否已撤销 + */ + private boolean revoked; + + /** + * 撤销时间 + */ + private Instant revokedAt; + + /** + * 关联的刷新Token ID + */ + private String refreshTokenId; + + /** + * JWT ID(如果是JWT token) + */ + private String jti; + + /** + * 附加数据(JSON格式) + */ + private String additionalInfo; + + /** + * 检查Token是否过期 + */ + public boolean isExpired() { + return expiresAt != null && Instant.now().isAfter(expiresAt); + } + + /** + * 检查Token是否有效 + */ + public boolean isValid() { + return !isExpired() && !revoked; + } + + /** + * 获取剩余有效时间(秒) + */ + public long getRemainingSeconds() { + if (expiresAt == null) { + return 0; + } + Instant now = Instant.now(); + if (now.isAfter(expiresAt)) { + return 0; + } + return expiresAt.getEpochSecond() - now.getEpochSecond(); + } + + /** + * 获取Token使用时长(秒) + */ + public long getUsedSeconds() { + if (issuedAt == null) { + return 0; + } + Instant end = revoked ? (revokedAt != null ? revokedAt : Instant.now()) : Instant.now(); + return end.getEpochSecond() - issuedAt.getEpochSecond(); + } + public Map toRevokeMap() { + Map hash = new HashMap<>(); + hash.put("revoked", Boolean.toString(revoked)); + hash.put("revokedAt", revokedAt != null ? revokedAt.toString() : ""); + return hash; + } + /** + * 转换为Map,便于Redis存储 + */ + public Map toMap() { + Map map = new HashMap<>(); + map.put("userId", userId); + map.put("username", username); + map.put("clientId", clientId); + map.put("tokenValue",tokenValue); + map.put("scopes", scopes != null ? String.join(",", scopes) : ""); + map.put("issuedAt", issuedAt != null ? issuedAt.toString() : null); + map.put("expiresAt", expiresAt != null ? expiresAt.toString() : null); + map.put("deviceId", deviceId); + map.put("deviceType", deviceType != null ? deviceType.name() : null); + map.put("ipAddress", ipAddress); + map.put("userAgent", userAgent); + map.put("revoked", Boolean.toString(revoked)); + map.put("revokedAt", revokedAt != null ? revokedAt.toString() : null); + map.put("refreshTokenId", refreshTokenId); + map.put("jti", jti); + map.put("additionalInfo", additionalInfo); + return map; + } + + /** + * 从Map创建AccessTokenInfo + */ + public static AccessTokenInfo fromMap(Map map) { + if (map == null || map.isEmpty()) { + return null; + } + + AccessTokenInfo.AccessTokenInfoBuilder builder = AccessTokenInfo.builder(); + + builder.userId((String) map.get("userId")); + builder.username((String) map.get("username")); + builder.clientId((String) map.get("clientId")); + builder.tokenValue((String) map.get("tokenValue")); + + // 处理scopes + String scopesStr = (String) map.get("scopes"); + if (scopesStr != null && !scopesStr.isEmpty()) { + builder.scopes(new HashSet<>(Arrays.asList(scopesStr.split(",")))); + } + + // 处理时间字段 + String issuedAtStr = (String) map.get("issuedAt"); + if (issuedAtStr != null) { + builder.issuedAt(Instant.parse(issuedAtStr)); + } + + String expiresAtStr = (String) map.get("expiresAt"); + if (expiresAtStr != null) { + builder.expiresAt(Instant.parse(expiresAtStr)); + } + + builder.deviceId((String) map.get("deviceId")); + + // 处理deviceType + String deviceTypeStr = (String) map.get("deviceType"); + if (deviceTypeStr != null) { + try { + builder.deviceType(DeviceType.valueOf(deviceTypeStr)); + } catch (IllegalArgumentException e) { + builder.deviceType(DeviceType.OTHER); + } + } + + builder.ipAddress((String) map.get("ipAddress")); + builder.userAgent((String) map.get("userAgent")); + + // 处理布尔值 + String revokedStr = (String) map.get("revoked"); + if (revokedStr != null) { + builder.revoked(Boolean.parseBoolean(revokedStr)); + } + + String revokedAtStr = (String) map.get("revokedAt"); + if (revokedAtStr != null) { + builder.revokedAt(Instant.parse(revokedAtStr)); + } + + builder.refreshTokenId((String) map.get("refreshTokenId")); + builder.jti((String) map.get("jti")); + builder.additionalInfo((String) map.get("additionalInfo")); + + return builder.build(); + } + + /** + * 简化的用户信息(用于接口返回) + */ + public Map toSimpleInfo() { + Map info = new HashMap<>(); + info.put("userId", userId); + info.put("username", username); + info.put("clientId", clientId); + info.put("scopes", scopes); + info.put("expiresAt", expiresAt != null ? expiresAt.toEpochMilli() : null); + info.put("issuedAt", issuedAt != null ? issuedAt.toEpochMilli() : null); + info.put("deviceType", deviceType != null ? deviceType.name() : null); + info.put("valid", isValid()); + return info; + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/DataPermission.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/DataPermission.java new file mode 100644 index 0000000..2c427c9 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/DataPermission.java @@ -0,0 +1,20 @@ +package org.lingniu.idp.model.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.Set; + +@Data +@Builder +public class DataPermission { + /** 允许全部*/ + private boolean allowAll; + /**仅自己*/ + private boolean onlySelf; + /**部门列表*/ + private Set deptList; + /**地区*/ + private Set areas; + +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/MetaVo.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/MetaVo.java new file mode 100644 index 0000000..fecd1e4 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/MetaVo.java @@ -0,0 +1,107 @@ +package org.lingniu.idp.model.vo; + + +import org.lingniu.idp.utils.StringUtils; + +/** + * 路由显示信息 + * + * @author portal + */ +public class MetaVo +{ + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + private String title; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + private String icon; + + /** + * 设置为true,则不会被 缓存 + */ + private boolean noCache; + + /** + * 内链地址(http(s)://开头) + */ + private String link; + + public MetaVo() + { + } + + public MetaVo(String title, String icon) + { + this.title = title; + this.icon = icon; + } + + public MetaVo(String title, String icon, boolean noCache) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + } + + public MetaVo(String title, String icon, String link) + { + this.title = title; + this.icon = icon; + this.link = link; + } + + public MetaVo(String title, String icon, boolean noCache, String link) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + if (StringUtils.ishttp(link)) + { + this.link = link; + } + } + + public boolean isNoCache() + { + return noCache; + } + + public void setNoCache(boolean noCache) + { + this.noCache = noCache; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public String getLink() + { + return link; + } + + public void setLink(String link) + { + this.link = link; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/RefreshTokenInfo.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/RefreshTokenInfo.java new file mode 100644 index 0000000..b3a5133 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/RefreshTokenInfo.java @@ -0,0 +1,436 @@ +package org.lingniu.idp.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.lingniu.idp.enums.DeviceType; +import org.lingniu.idp.enums.RevokeReason; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Arrays; + +/** + * Refresh Token 信息 + * 存储在 Redis Hash 结构中 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RefreshTokenInfo { + private String tokenValue; + + /** + * 用户ID + */ + private String userId; + + /** + * 用户名 + */ + private String username; + + /** + * 客户端ID + */ + private String clientId; + + /** + * 设备ID(唯一标识设备) + */ + private String deviceId; + + /** + * 设备名称(用户自定义) + */ + private String deviceName; + + /** + * 设备类型 + */ + private DeviceType deviceType; + + /** + * 操作系统 + */ + private String operatingSystem; + + /** + * 浏览器/客户端类型 + */ + private String clientType; + + /** + * 权限范围 + */ + private Set scopes; + + /** + * 创建时间 + */ + private Instant createdAt; + + /** + * 最后使用时间 + */ + private Instant lastUsedAt; + + /** + * 过期时间 + */ + private Instant expiresAt; + + /** + * IP地址(创建时的IP) + */ + private String ipAddress; + + /** + * 用户代理(创建时的User-Agent) + */ + private String userAgent; + + /** + * 是否已撤销 + */ + private boolean revoked; + + /** + * 撤销时间 + */ + private Instant revokedAt; + + /** + * 撤销原因 + */ + private RevokeReason revokeReason; + /** + * 对应accessToken + */ + private String accessToken; + /** + * 关联的Access Token数量(用于统计) + */ + private int accessTokenCount; + + /** + * 使用次数 + */ + private int usageCount; + + /** + * 地理位置信息(可选) + */ + private String location; + + /** + * 附加数据(JSON格式) + */ + private String additionalInfo; + + /** + * 是否记住登录 + */ + private boolean rememberMe; + + + + + + /** + * 检查Refresh Token是否过期 + */ + public boolean isExpired() { + return expiresAt != null && Instant.now().isAfter(expiresAt); + } + + /** + * 检查Refresh Token是否有效 + */ + public boolean isValid() { + return !isExpired() && !revoked; + } + + /** + * 获取剩余有效时间(秒) + */ + public long getRemainingSeconds() { + if (expiresAt == null) { + return 0; + } + Instant now = Instant.now(); + if (now.isAfter(expiresAt)) { + return 0; + } + return expiresAt.getEpochSecond() - now.getEpochSecond(); + } + + /** + * 获取活跃天数(创建到现在) + */ + public long getActiveDays() { + if (createdAt == null) { + return 0; + } + Instant end = revoked ? (revokedAt != null ? revokedAt : Instant.now()) : Instant.now(); + long seconds = end.getEpochSecond() - createdAt.getEpochSecond(); + return seconds / (24 * 3600); + } + + /** + * 获取闲置天数(最后使用到现在) + */ + public long getIdleDays() { + if (lastUsedAt == null) { + return getActiveDays(); + } + Instant now = Instant.now(); + long seconds = now.getEpochSecond() - lastUsedAt.getEpochSecond(); + return seconds / (24 * 3600); + } + + /** + * 增加使用计数 + */ + public void incrementUsage() { + this.usageCount++; + this.lastUsedAt = Instant.now(); + } + + public void incrementAccessTokenUsage(String accessToken) { + this.accessTokenCount++; + this.accessToken = accessToken; + } + + /** + * 撤销Token + */ + public void revoke(RevokeReason reason) { + this.revoked = true; + this.revokedAt = Instant.now(); + this.revokeReason = reason; + } + + /** + * 转换为Map,便于Redis存储 + */ + public Map toMap() { + Map hash = new HashMap<>(); + + hash.put("userId", userId != null ? userId : ""); + hash.put("username", username != null ? username : ""); + hash.put("clientId", clientId != null ? clientId : ""); + hash.put("deviceId", deviceId != null ? deviceId : ""); + hash.put("tokenValue", tokenValue != null ? tokenValue : ""); + hash.put("deviceName", deviceName != null ? deviceName : ""); + hash.put("deviceType", deviceType != null ? deviceType.name() : DeviceType.OTHER.name()); + hash.put("operatingSystem", operatingSystem != null ? operatingSystem : ""); + hash.put("clientType", clientType != null ? clientType : ""); + hash.put("accessToken", accessToken != null ? accessToken : ""); + hash.put("scopes", scopes != null ? String.join(",", scopes) : ""); + hash.put("createdAt", createdAt != null ? createdAt.toString() : ""); + hash.put("lastUsedAt", lastUsedAt != null ? lastUsedAt.toString() : ""); + hash.put("expiresAt", expiresAt != null ? expiresAt.toString() : ""); + hash.put("ipAddress", ipAddress != null ? ipAddress : ""); + hash.put("userAgent", userAgent != null ? userAgent : ""); + hash.put("revoked", Boolean.toString(revoked)); + hash.put("revokedAt", revokedAt != null ? revokedAt.toString() : ""); + hash.put("revokeReason", revokeReason != null ? revokeReason.name() : ""); + hash.put("accessTokenCount", Integer.toString(accessTokenCount)); + hash.put("usageCount", Integer.toString(usageCount)); + hash.put("location", location != null ? location : ""); + hash.put("additionalInfo", additionalInfo != null ? additionalInfo : ""); + hash.put("rememberMe", Boolean.toString(rememberMe)); + + return hash; + } + public Map toUpdateMap() { + Map hash = new HashMap<>(); + hash.put("lastUsedAt", lastUsedAt != null ? lastUsedAt.toString() : ""); + hash.put("accessTokenCount", Integer.toString(accessTokenCount)); + hash.put("accessToken", accessToken != null ? accessToken : ""); + hash.put("usageCount", Integer.toString(usageCount)); + return hash; + } + public Map toRevokeMap() { + Map hash = new HashMap<>(); + hash.put("revoked", Boolean.toString(revoked)); + hash.put("revokedAt", revokedAt != null ? revokedAt.toString() : ""); + hash.put("revokeReason", revokeReason != null ? revokeReason.name() : ""); + return hash; + } + + /** + * 从Redis Hash创建RefreshTokenInfo + */ + public static RefreshTokenInfo fromMap(Map hash) { + if (hash == null || hash.isEmpty()) { + return null; + } + + RefreshTokenInfoBuilder builder = RefreshTokenInfo.builder(); + + builder.userId((String) hash.getOrDefault("userId", "")); + builder.username((String) hash.getOrDefault("username", "")); + builder.clientId((String) hash.getOrDefault("clientId", "")); + builder.deviceId((String) hash.getOrDefault("deviceId", "")); + builder.deviceName((String) hash.getOrDefault("deviceName", "")); + builder.accessToken((String) hash.getOrDefault("accessToken", "")); + builder.tokenValue((String) hash.getOrDefault("tokenValue", "")); + + // 处理deviceType + String deviceTypeStr = (String)hash.get("deviceType"); + if (deviceTypeStr != null && !deviceTypeStr.isEmpty()) { + try { + builder.deviceType(DeviceType.valueOf(deviceTypeStr)); + } catch (IllegalArgumentException e) { + builder.deviceType(DeviceType.OTHER); + } + } else { + builder.deviceType(DeviceType.OTHER); + } + + builder.operatingSystem((String) hash.getOrDefault("operatingSystem", "")); + builder.clientType((String) hash.getOrDefault("clientType", "")); + + // 处理scopes + String scopesStr = (String)hash.get("scopes"); + if (scopesStr != null && !scopesStr.isEmpty()) { + builder.scopes(new HashSet<>(Arrays.asList(scopesStr.split(",")))); + } else { + builder.scopes(new HashSet<>()); + } + + // 处理时间字段 + String createdAtStr = (String)hash.get("createdAt"); + if (createdAtStr != null && !createdAtStr.isEmpty()) { + try { + builder.createdAt(Instant.parse(createdAtStr)); + } catch (Exception e) { + // 解析失败,使用当前时间 + builder.createdAt(Instant.now()); + } + } + + String lastUsedAtStr = (String)hash.get("lastUsedAt"); + if (lastUsedAtStr != null && !lastUsedAtStr.isEmpty()) { + try { + builder.lastUsedAt(Instant.parse(lastUsedAtStr)); + } catch (Exception e) { + // 解析失败,忽略 + } + } + + String expiresAtStr = (String)hash.get("expiresAt"); + if (expiresAtStr != null && !expiresAtStr.isEmpty()) { + try { + builder.expiresAt(Instant.parse(expiresAtStr)); + } catch (Exception e) { + // 解析失败,忽略 + } + } + + builder.ipAddress((String) hash.getOrDefault("ipAddress", "")); + builder.userAgent((String) hash.getOrDefault("userAgent", "")); + + // 处理布尔值 + String revokedStr = (String)hash.get("revoked"); + builder.revoked(Boolean.parseBoolean(revokedStr)); + + String revokedAtStr = (String)hash.get("revokedAt"); + if (revokedAtStr != null && !revokedAtStr.isEmpty()) { + try { + builder.revokedAt(Instant.parse(revokedAtStr)); + } catch (Exception e) { + // 解析失败,忽略 + } + } + + // 处理撤销原因 + String revokeReasonStr = (String)hash.get("revokeReason"); + if (revokeReasonStr != null && !revokeReasonStr.isEmpty()) { + try { + builder.revokeReason(RevokeReason.valueOf(revokeReasonStr)); + } catch (IllegalArgumentException e) { + builder.revokeReason(RevokeReason.OTHER); + } + } + + // 处理数值字段 + String accessTokenCountStr = (String)hash.get("accessTokenCount"); + if (accessTokenCountStr != null && !accessTokenCountStr.isEmpty()) { + try { + builder.accessTokenCount(Integer.parseInt(accessTokenCountStr)); + } catch (NumberFormatException e) { + builder.accessTokenCount(0); + } + } + + String usageCountStr = (String)hash.get("usageCount"); + if (usageCountStr != null && !usageCountStr.isEmpty()) { + try { + builder.usageCount(Integer.parseInt(usageCountStr)); + } catch (NumberFormatException e) { + builder.usageCount(0); + } + } + + builder.location((String) hash.getOrDefault("location", "")); + builder.additionalInfo((String) hash.getOrDefault("additionalInfo", "")); + + String rememberMeStr = (String)hash.get("rememberMe"); + builder.rememberMe(Boolean.parseBoolean(rememberMeStr)); + + return builder.build(); + } + + /** + * 简化的设备信息(用于前端展示) + */ + public Map toSimpleDeviceInfo() { + Map info = new HashMap<>(); + info.put("deviceId", deviceId); + info.put("deviceName", deviceName); + info.put("deviceType", deviceType != null ? deviceType.name() : null); + info.put("deviceTypeDesc", deviceType != null ? deviceType.getDescription() : null); + info.put("operatingSystem", operatingSystem); + info.put("clientType", clientType); + info.put("ipAddress", ipAddress); + info.put("location", location); + info.put("createdAt", createdAt != null ? createdAt.toEpochMilli() : null); + info.put("lastUsedAt", lastUsedAt != null ? lastUsedAt.toEpochMilli() : null); + info.put("active", !revoked && !isExpired()); + return info; + } + + /** + * 获取可读的设备信息 + */ + public String getReadableDeviceInfo() { + StringBuilder sb = new StringBuilder(); + + if (deviceName != null && !deviceName.isEmpty()) { + sb.append(deviceName); + } else if (deviceType != null) { + sb.append(deviceType.getDescription()); + } + + if (operatingSystem != null && !operatingSystem.isEmpty()) { + sb.append(" (").append(operatingSystem); + if (clientType != null && !clientType.isEmpty()) { + sb.append(" - ").append(clientType); + } + sb.append(")"); + } else if (clientType != null && !clientType.isEmpty()) { + sb.append(" (").append(clientType).append(")"); + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/RouterVo.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/RouterVo.java new file mode 100644 index 0000000..e07ecbd --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/RouterVo.java @@ -0,0 +1,149 @@ +package org.lingniu.idp.model.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +/** + * 路由配置信息 + * + * @author portal + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouterVo +{ + /** + * 路由名字 + */ + private String name; + + /** + * 路由地址 + */ + private String path; + + /** + * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现 + */ + private boolean hidden; + + /** + * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + */ + private String redirect; + + /** + * 组件地址 + */ + private String component; + + /** + * 路由参数:如 {"id": 1, "name": "admin"} + */ + private String query; + + /** + * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + */ + private Boolean alwaysShow; + + /** + * 其他元素 + */ + private MetaVo meta; + + /** + * 子路由 + */ + private List children; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + public boolean getHidden() + { + return hidden; + } + + public void setHidden(boolean hidden) + { + this.hidden = hidden; + } + + public String getRedirect() + { + return redirect; + } + + public void setRedirect(String redirect) + { + this.redirect = redirect; + } + + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public Boolean getAlwaysShow() + { + return alwaysShow; + } + + public void setAlwaysShow(Boolean alwaysShow) + { + this.alwaysShow = alwaysShow; + } + + public MetaVo getMeta() + { + return meta; + } + + public void setMeta(MetaVo meta) + { + this.meta = meta; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/TokenInfo.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/TokenInfo.java new file mode 100644 index 0000000..bb7d974 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/model/vo/TokenInfo.java @@ -0,0 +1,20 @@ +package org.lingniu.idp.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +// TokenInfo.java +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TokenInfo implements Serializable { + + private static final long serialVersionUID = 1L; + private AccessTokenInfo accessTokenInfo; + private RefreshTokenInfo refreshTokenInfo; +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/context/AuthenticationContextHolder.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/context/AuthenticationContextHolder.java new file mode 100644 index 0000000..eb3686b --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/context/AuthenticationContextHolder.java @@ -0,0 +1,28 @@ +package org.lingniu.idp.security.context; + +import org.springframework.security.core.Authentication; + +/** + * 身份验证信息 + * + * @author portal + */ +public class AuthenticationContextHolder +{ + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static Authentication getContext() + { + return contextHolder.get(); + } + + public static void setContext(Authentication context) + { + contextHolder.set(context); + } + + public static void clearContext() + { + contextHolder.remove(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/context/PermissionContextHolder.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/context/PermissionContextHolder.java new file mode 100644 index 0000000..adad05d --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/context/PermissionContextHolder.java @@ -0,0 +1,27 @@ +package org.lingniu.idp.security.context; + +import org.lingniu.idp.utils.text.Convert; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +/** + * 权限信息 + * + * @author portal + */ +public class PermissionContextHolder +{ + private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT"; + + public static void setContext(String permission) + { + RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission, + RequestAttributes.SCOPE_REQUEST); + } + + public static String getContext() + { + return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES, + RequestAttributes.SCOPE_REQUEST)); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/converter/SmsCodeAuthenticationConverter.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/converter/SmsCodeAuthenticationConverter.java new file mode 100644 index 0000000..697b49b --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/converter/SmsCodeAuthenticationConverter.java @@ -0,0 +1,25 @@ +package org.lingniu.idp.security.converter; + +import jakarta.servlet.http.HttpServletRequest; +import org.lingniu.idp.security.token.SmsCodeAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class SmsCodeAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + // 从请求参数中获取手机号和验证码 + String phone = request.getParameter("phone"); + String code = request.getParameter("code"); + + if (StringUtils.hasText(phone) && StringUtils.hasText(code)) { + return new SmsCodeAuthenticationToken(phone, code); + } + + return null; + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/filter/IdpAuthenticationFilter.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/filter/IdpAuthenticationFilter.java new file mode 100644 index 0000000..87aa609 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/filter/IdpAuthenticationFilter.java @@ -0,0 +1,83 @@ +package org.lingniu.idp.security.filter; + +import org.lingniu.idp.model.vo.AccessTokenInfo; +import org.lingniu.idp.service.core.login.IdpTokenService; +import org.lingniu.idp.service.core.login.RedisAccessTokenService; +import org.springframework.core.log.LogMessage; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class IdpAuthenticationFilter extends OncePerRequestFilter { + private static final RequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults() + .matcher(HttpMethod.POST, "/oauth2/authorize"); + private final IdpTokenService idpTokenService; + private final UserDetailsService userDetailsService; + + public IdpAuthenticationFilter(IdpTokenService idpTokenService, UserDetailsService userDetailsService) { + this.idpTokenService = idpTokenService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + if (!requiresAuthentication(request, response)) { + filterChain.doFilter(request, response); + return; + } + + try { + AccessTokenInfo accessTokenInfo = null; + // 验证令牌 + if (idpTokenService.validateAccessToken(request)) { + accessTokenInfo = idpTokenService.getAccessTokenInfo(request); + }else{ + accessTokenInfo = idpTokenService.refreshToken(request, response); + } + if(accessTokenInfo!=null){ + // 加载用户详情 + UserDetails userDetails = userDetailsService.loadUserByUsername(accessTokenInfo.getUsername()); + // 创建认证对象 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, + null, + null + ); + + // 设置认证信息到SecurityContext + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + } catch (Exception e) { + // 令牌验证失败,记录日志但不中断请求 + logger.error("token 验证失败", e); + } + + filterChain.doFilter(request, response); + } + protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { + if (DEFAULT_ANT_PATH_REQUEST_MATCHER.matches(request)) { + return true; + } + if (this.logger.isTraceEnabled()) { + this.logger + .trace(LogMessage.format("Did not match request to %s", DEFAULT_ANT_PATH_REQUEST_MATCHER)); + } + return false; + } + +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/filter/login/MobilePasswordAuthenticationFilter.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/filter/login/MobilePasswordAuthenticationFilter.java new file mode 100644 index 0000000..2de9168 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/filter/login/MobilePasswordAuthenticationFilter.java @@ -0,0 +1,35 @@ +package org.lingniu.idp.security.filter.login; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.lingniu.idp.security.token.MobilePasswordAuthenticationToken; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +public class MobilePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + private static final RequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults() + .matcher(HttpMethod.POST, "/api/login/account"); + public MobilePasswordAuthenticationFilter() { + super(DEFAULT_ANT_PATH_REQUEST_MATCHER); + } + + public MobilePasswordAuthenticationFilter(AuthenticationManager authenticationManager) { + super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); + } + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){ + String username = request.getParameter("username"); + String password = request.getParameter("password"); + String code = request.getParameter("code"); + String uuid = request.getParameter("uuid"); + MobilePasswordAuthenticationToken mobilePasswordAuthenticationToken = + new MobilePasswordAuthenticationToken(username,password,code,uuid); + // 2. 进行认证 + Authentication authenticate = this.getAuthenticationManager().authenticate(mobilePasswordAuthenticationToken); + return authenticate; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/granttype/CaptchaCodeGrantType.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/granttype/CaptchaCodeGrantType.java new file mode 100644 index 0000000..4293ce1 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/granttype/CaptchaCodeGrantType.java @@ -0,0 +1,7 @@ +package org.lingniu.idp.security.granttype; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; + +public class CaptchaCodeGrantType { + public static final AuthorizationGrantType SMS_CODE = new AuthorizationGrantType("captcha_code"); +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/granttype/SmsCodeGrantType.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/granttype/SmsCodeGrantType.java new file mode 100644 index 0000000..05fa445 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/granttype/SmsCodeGrantType.java @@ -0,0 +1,7 @@ +package org.lingniu.idp.security.granttype; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; + +public class SmsCodeGrantType { + public static final AuthorizationGrantType SMS_CODE = new AuthorizationGrantType("sms_code"); +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/AuthenticationEntryPointImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..4a84421 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,35 @@ +package org.lingniu.idp.security.handler; + +import com.alibaba.fastjson2.JSON; +import org.lingniu.idp.model.base.AjaxResult; +import org.lingniu.idp.utils.ServletUtils; +import org.lingniu.idp.utils.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Serializable; + +/** + * 认证失败处理类 返回未授权 + * + * @author portal + */ +@Component +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable +{ + private static final long serialVersionUID = -8970718410437077606L; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException + { + int code = HttpStatus.UNAUTHORIZED.value(); + String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/GlobalExceptionHandler.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..0a1dfdc --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/GlobalExceptionHandler.java @@ -0,0 +1,145 @@ +package org.lingniu.idp.security.handler; +import jakarta.servlet.http.HttpServletRequest; +import org.lingniu.idp.exception.DemoModeException; +import org.lingniu.idp.exception.ServiceException; +import org.lingniu.idp.model.base.AjaxResult; +import org.lingniu.idp.utils.EscapeUtil; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.utils.text.Convert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + + +/** + * 全局异常处理器 + * + * @author portal + */ +@RestControllerAdvice +public class GlobalExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 权限校验异常 + */ + @ExceptionHandler(AccessDeniedException.class) + public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.FORBIDDEN.value(), "没有权限,请联系管理员授权"); + } + + /** + * 请求方式不支持 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, + HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); + return AjaxResult.error(e.getMessage()); + } + + /** + * 业务异常 + */ + @ExceptionHandler(ServiceException.class) + public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) + { + log.error(e.getMessage(), e); + Integer code = e.getCode(); + return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); + } + + /** + * 请求路径中缺少必需的路径变量 + */ + @ExceptionHandler(MissingPathVariableException.class) + public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName())); + } + + /** + * 请求参数类型不匹配 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + String value = Convert.toStr(e.getValue()); + if (StringUtils.isNotEmpty(value)) + { + value = EscapeUtil.clean(value); + } + log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value)); + } + + /** + * 拦截未知的运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生未知异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 系统异常 + */ + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public AjaxResult handleBindException(BindException e) + { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) + { + log.error(e.getMessage(), e); + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 演示模式异常 + */ + @ExceptionHandler(DemoModeException.class) + public AjaxResult handleDemoModeException(DemoModeException e) + { + return AjaxResult.error("演示模式,不允许操作"); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/LoginSuccessHandler.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/LoginSuccessHandler.java new file mode 100644 index 0000000..841398f --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/LoginSuccessHandler.java @@ -0,0 +1,47 @@ +package org.lingniu.idp.security.handler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.manager.AsyncManager; +import org.lingniu.idp.manager.factory.AsyncFactory; +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.model.vo.TokenInfo; +import org.lingniu.idp.service.core.login.IdpTokenService; +import org.lingniu.idp.utils.MessageUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class LoginSuccessHandler implements AuthenticationSuccessHandler { + + private final IdpTokenService idpTokenService; + + public LoginSuccessHandler(IdpTokenService idpTokenService) { + this.idpTokenService = idpTokenService; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + + // 生成token + TokenInfo token = idpTokenService.createToken(request, loginUser); + // 保存token + idpTokenService.storeTokenInfo(token); + // 将短token放入响应头 + idpTokenService.setAccessTokenHeader(response,token.getAccessTokenInfo().getTokenValue()); + // 设置Refresh Token到HttpOnly Cookie + idpTokenService.setRefreshTokenCookie(response, token); + // 记录审计日志 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginUser.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + // 保存日志 + idpTokenService.recordLoginInfo(loginUser.getUserId()); + } + + +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/LogoutSuccessHandlerImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/LogoutSuccessHandlerImpl.java new file mode 100644 index 0000000..3cd6103 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/LogoutSuccessHandlerImpl.java @@ -0,0 +1,51 @@ +package org.lingniu.idp.security.handler; + +import com.alibaba.fastjson2.JSON; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.manager.AsyncManager; +import org.lingniu.idp.manager.factory.AsyncFactory; +import org.lingniu.idp.model.base.AjaxResult; +import org.lingniu.idp.model.vo.RefreshTokenInfo; +import org.lingniu.idp.service.core.login.IdpTokenService; +import org.lingniu.idp.utils.MessageUtils; +import org.lingniu.idp.utils.ServletUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import java.io.IOException; + +/** + * 自定义退出处理类 返回成功 + * + * @author portal + */ +@Configuration +public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler +{ + @Autowired + private IdpTokenService idpTokenService; + + /** + * 退出处理 + * + * @return + */ + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throws IOException, ServletException + { + RefreshTokenInfo refreshTokenInfo = idpTokenService.getRefreshTokenInfo(request); + idpTokenService.revokeToken(request,response); + if (refreshTokenInfo!=null) + { + String userName = refreshTokenInfo.getUsername(); + // 记录用户退出日志 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success"))); + } + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success")))); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/Oauth2CodeSuccessHandler.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/Oauth2CodeSuccessHandler.java new file mode 100644 index 0000000..9173f10 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/handler/Oauth2CodeSuccessHandler.java @@ -0,0 +1,49 @@ +package org.lingniu.idp.security.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.lingniu.idp.model.base.CommonResult; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class Oauth2CodeSuccessHandler implements AuthenticationSuccessHandler { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + OAuth2AuthorizationCodeRequestAuthenticationToken authCodeRequest = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication; + + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUriString(authCodeRequest.getRedirectUri()) + .queryParam(OAuth2ParameterNames.CODE, + authCodeRequest.getAuthorizationCode().getTokenValue()); + + if (StringUtils.hasText(authCodeRequest.getState())) { + uriBuilder.queryParam(OAuth2ParameterNames.STATE, + UriUtils.encode(authCodeRequest.getState(), StandardCharsets.UTF_8)); + } + + String redirectUri = uriBuilder.build(true).toUriString(); + + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.setStatus(HttpStatus.OK.value()); + + objectMapper.writeValue(response.getWriter(), CommonResult.success(Map.of("redirect_url", redirectUri))); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/provider/MobilePasswordAuthenticationProvider.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/provider/MobilePasswordAuthenticationProvider.java new file mode 100644 index 0000000..0221f6d --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/provider/MobilePasswordAuthenticationProvider.java @@ -0,0 +1,45 @@ +package org.lingniu.idp.security.provider; + +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.security.token.MobilePasswordAuthenticationToken; +import org.lingniu.idp.service.core.login.LoginService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; + +import java.util.Collections; + +@Component +public class MobilePasswordAuthenticationProvider implements AuthenticationProvider { + + @Autowired + private UserDetailsService userDetailsService; + @Autowired + private LoginService loginService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + MobilePasswordAuthenticationToken authToken = (MobilePasswordAuthenticationToken) authentication; + String username = (String) authToken.getPrincipal(); + String password = (String) authToken.getCredentials(); + String code = authToken.getCode(); + String uuid = authToken.getUuid(); + + // 2. 加载用户 + LoginUser login = loginService.login(username, password, code, uuid); + + // 3. 返回认证成功的Token + return new MobilePasswordAuthenticationToken( + login, + null + ); + } + + @Override + public boolean supports(Class authentication) { + return MobilePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/provider/SmsCodeAuthenticationProvider.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/provider/SmsCodeAuthenticationProvider.java new file mode 100644 index 0000000..1256b48 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/provider/SmsCodeAuthenticationProvider.java @@ -0,0 +1,74 @@ +package org.lingniu.idp.security.provider; + +import org.lingniu.idp.security.token.SmsCodeAuthenticationToken; +import org.lingniu.idp.service.core.SmsCodeService; +import org.lingniu.idp.service.core.UserDetailsServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +@Component +public class SmsCodeAuthenticationProvider implements AuthenticationProvider { + + @Autowired + private SmsCodeService smsCodeService; + + @Autowired + private UserDetailsServiceImpl userDetailsService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; + String phone = (String) authenticationToken.getPrincipal(); + String code = (String) authenticationToken.getCredentials(); + + // 1. 验证短信验证码 + if (!smsCodeService.verifyCode(phone, code)) { + throw new BadCredentialsException("验证码错误或已过期"); + } + + // 2. 根据手机号查找用户 + UserDetails userDetails; + try { + // 这里可以根据手机号从数据库查询用户 + userDetails = loadUserByPhone(phone); + } catch (UsernameNotFoundException e) { + // 用户不存在,可以自动创建用户 + userDetails = createNewUser(phone); + } + + // 3. 返回认证成功的Token + return new SmsCodeAuthenticationToken( + userDetails, + null + ); + } + + @Override + public boolean supports(Class authentication) { + return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); + } + + private UserDetails loadUserByPhone(String phone) { + // 这里实现根据手机号查询用户的逻辑 + // 可以查询数据库,或者调用用户服务 + + // 示例:假设手机号作为用户名 + return userDetailsService.loadUserByUsername(phone); + } + + private UserDetails createNewUser(String phone) { + // 用户不存在时自动创建 + return User.builder() + .username(phone) + .password("{noop}") // 无密码 + .authorities("ROLE_USER") + .build(); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/token/BaseAuthenticationToken.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/token/BaseAuthenticationToken.java new file mode 100644 index 0000000..470663a --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/token/BaseAuthenticationToken.java @@ -0,0 +1,70 @@ +package org.lingniu.idp.security.token; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +// 基础认证Token +public abstract class BaseAuthenticationToken extends AbstractAuthenticationToken { + protected final Object principal; + protected String credentials; + protected String deviceId; + protected String deviceType; + protected String clientId; + + public BaseAuthenticationToken(Object principal, Object credentials) { + super(null); + this.principal = principal; + this.credentials = (String) credentials; + setAuthenticated(false); + } + + public BaseAuthenticationToken(Object principal, Object credentials, + Collection authorities) { + super(authorities); + this.principal = principal; + this.credentials = (String) credentials; + super.setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceType(String deviceType) { + this.deviceType = deviceType; + } + + public String getDeviceType() { + return deviceType; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientId() { + return clientId; + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + credentials = null; + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/token/MobilePasswordAuthenticationToken.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/token/MobilePasswordAuthenticationToken.java new file mode 100644 index 0000000..cef4a50 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/token/MobilePasswordAuthenticationToken.java @@ -0,0 +1,31 @@ +package org.lingniu.idp.security.token; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +// 手机号+密码+验证码登录Token +public class MobilePasswordAuthenticationToken extends BaseAuthenticationToken { + private String code; + private String uuid; + + public MobilePasswordAuthenticationToken(String mobile, String password, String code, String uuid) { + super(mobile, password); + this.code = code; + this.uuid = uuid; + } + + public MobilePasswordAuthenticationToken(UserDetails userDetails, + Collection authorities) { + super(userDetails, "", authorities); + } + + public String getCode() { + return code; + } + + public String getUuid() { + return uuid; + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/token/SmsCodeAuthenticationToken.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/token/SmsCodeAuthenticationToken.java new file mode 100644 index 0000000..f55c203 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/security/token/SmsCodeAuthenticationToken.java @@ -0,0 +1,45 @@ +package org.lingniu.idp.security.token; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { + + private final Object principal; + private final String code; + + public SmsCodeAuthenticationToken(String phone, String code) { + super(null); + this.principal = phone; + this.code = code; + setAuthenticated(false); + } + + public SmsCodeAuthenticationToken(Object principal, Collection authorities) { + super(authorities); + this.principal = principal; + this.code = null; + super.setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return code; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + if (isAuthenticated) { + throw new IllegalArgumentException( + "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); + } + super.setAuthenticated(false); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysConfigService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysConfigService.java new file mode 100644 index 0000000..b2442d2 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysConfigService.java @@ -0,0 +1,89 @@ +package org.lingniu.idp.service; + +import java.util.List; +import org.lingniu.idp.model.entity.SysConfig; + +/** + * 参数配置 服务层 + * + * @author portal + */ +public interface ISysConfigService +{ + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数键值 + */ + public String selectConfigByKey(String configKey); + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + public boolean selectCaptchaEnabled(); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + public void deleteConfigByIds(Long[] configIds); + + /** + * 加载参数缓存数据 + */ + public void loadingConfigCache(); + + /** + * 清空参数缓存数据 + */ + public void clearConfigCache(); + + /** + * 重置参数缓存数据 + */ + public void resetConfigCache(); + + /** + * 校验参数键名是否唯一 + * + * @param config 参数信息 + * @return 结果 + */ + public boolean checkConfigKeyUnique(SysConfig config); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysDeptService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysDeptService.java new file mode 100644 index 0000000..0f6cf14 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysDeptService.java @@ -0,0 +1,128 @@ +package org.lingniu.idp.service; + +import java.util.List; +import org.lingniu.idp.model.base.TreeSelect; +import org.lingniu.idp.model.entity.SysDept; + +/** + * 部门管理 服务层 + * + * @author portal + */ +public interface ISysDeptService +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + public List selectDeptTreeList(SysDept dept); + + public List selectChildrenDeptById(Long deptId); + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + public List buildDeptTree(List depts); + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + public List buildDeptTreeSelect(List depts); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(Long roleId); + + List selectDeptListByUserRole(Long userId); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在部门子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public boolean hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + public boolean checkDeptNameUnique(SysDept dept); + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + public void checkDeptDataScope(Long deptId); + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysLogininforService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysLogininforService.java new file mode 100644 index 0000000..523f4ed --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysLogininforService.java @@ -0,0 +1,40 @@ +package org.lingniu.idp.service; + +import java.util.List; +import org.lingniu.idp.model.entity.SysLogininfor; + +/** + * 系统访问日志情况信息 服务层 + * + * @author portal + */ +public interface ISysLogininforService +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + */ + public void cleanLogininfor(); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysMenuService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysMenuService.java new file mode 100644 index 0000000..4dfebaa --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysMenuService.java @@ -0,0 +1,144 @@ +package org.lingniu.idp.service; + +import java.util.List; +import java.util.Set; +import org.lingniu.idp.model.base.TreeSelect; +import org.lingniu.idp.model.entity.SysMenu; +import org.lingniu.idp.model.vo.RouterVo; + +/** + * 菜单 业务层 + * + * @author portal + */ +public interface ISysMenuService +{ + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(Long userId); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu, Long userId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectMenuPermsByUserId(Long userId); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public Set selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询菜单树信息 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(Long roleId); + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + public List buildMenus(List menus); + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildMenuTree(List menus); + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + public List buildMenuTreeSelect(List menus); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean hasChildByMenuId(Long menuId); + + /** + * 查询菜单是否存在角色 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkMenuExistRole(Long menuId); + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean checkMenuNameUnique(SysMenu menu); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysOperLogService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysOperLogService.java new file mode 100644 index 0000000..1fa8130 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysOperLogService.java @@ -0,0 +1,48 @@ +package org.lingniu.idp.service; + +import java.util.List; +import org.lingniu.idp.model.entity.SysOperLog; + +/** + * 操作日志 服务层 + * + * @author portal + */ +public interface ISysOperLogService +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysPostService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysPostService.java new file mode 100644 index 0000000..1adbfc7 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysPostService.java @@ -0,0 +1,99 @@ +package org.lingniu.idp.service; + +import java.util.List; +import org.lingniu.idp.model.entity.SysPost; + +/** + * 岗位信息 服务层 + * + * @author portal + */ +public interface ISysPostService +{ + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位列表 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 校验岗位名称 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostNameUnique(SysPost post); + + /** + * 校验岗位编码 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostCodeUnique(SysPost post); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysRoleService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysRoleService.java new file mode 100644 index 0000000..7901294 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysRoleService.java @@ -0,0 +1,173 @@ +package org.lingniu.idp.service; + +import java.util.List; +import java.util.Set; +import org.lingniu.idp.model.entity.SysRole; +import org.lingniu.idp.model.entity.SysUserRole; + +/** + * 角色业务层 + * + * @author portal + */ +public interface ISysRoleService +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色列表 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolesByUserId(Long userId); + + /** + * 根据用户ID查询角色权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleNameUnique(SysRole role); + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleKeyUnique(SysRole role); + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + public void checkRoleAllowed(SysRole role); + + /** + * 校验角色是否有数据权限 + * + * @param roleIds 角色id + */ + public void checkRoleDataScope(Long... roleIds); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRoleStatus(SysRole role); + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int authDataScope(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteAuthUser(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + public int deleteAuthUsers(Long roleId, Long[] userIds); + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int insertAuthUsers(Long roleId, Long[] userIds); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysUserOnlineService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysUserOnlineService.java new file mode 100644 index 0000000..905ad44 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysUserOnlineService.java @@ -0,0 +1,48 @@ +package org.lingniu.idp.service; + +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.model.entity.SysUserOnline; + +/** + * 在线用户 服务层 + * + * @author portal + */ +public interface ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user); + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user); + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user); + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + public SysUserOnline loginUserToUserOnline(LoginUser user); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysUserService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysUserService.java new file mode 100644 index 0000000..0b613b6 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/ISysUserService.java @@ -0,0 +1,217 @@ +package org.lingniu.idp.service; + +import java.util.Date; +import java.util.List; +import org.lingniu.idp.model.entity.SysUser; + +/** + * 用户 业务层 + * + * @author portal + */ +public interface ISysUserService +{ + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser user); + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 根据用户ID查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserRoleGroup(String userName); + + /** + * 根据用户ID查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserPostGroup(String userName); + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkUserNameUnique(SysUser user); + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkPhoneUnique(SysUser user); + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkEmailUnique(SysUser user); + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + public void checkUserAllowed(SysUser user); + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + public void checkUserDataScope(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean registerUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserAuth(Long userId, Long[] roleIds); + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserStatus(SysUser user); + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserProfile(SysUser user); + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + public boolean updateUserAvatar(Long userId, String avatar); + + /** + * 更新用户登录信息(IP和登录时间) + * + * @param userId 用户ID + * @param loginIp 登录IP地址 + * @param loginDate 登录时间 + * @return 结果 + */ + public void updateLoginInfo(Long userId, String loginIp, Date loginDate); + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + public int resetPwd(SysUser user); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(Long userId, String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + public String importUser(List userList, Boolean isUpdateSupport, String operName); +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/CaptchaService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/CaptchaService.java new file mode 100644 index 0000000..f70b29a --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/CaptchaService.java @@ -0,0 +1,72 @@ +package org.lingniu.idp.service.core; + +import jakarta.annotation.Resource; +import org.lingniu.idp.common.redis.RedisCache; +import org.lingniu.idp.constant.CacheConstants; +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.constant.UserConstants; +import org.lingniu.idp.exception.ServiceException; +import org.lingniu.idp.exception.user.*; +import org.lingniu.idp.manager.AsyncManager; +import org.lingniu.idp.manager.factory.AsyncFactory; +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.security.context.AuthenticationContextHolder; +import org.lingniu.idp.service.ISysConfigService; +import org.lingniu.idp.service.ISysUserService; +import org.lingniu.idp.utils.DateUtils; +import org.lingniu.idp.utils.MessageUtils; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.utils.ip.IpUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + + +/** + * 登录校验方法 + * + * @author portal + */ +@Component +public class CaptchaService +{ + @Autowired + private RedisCache redisCache; + + + @Autowired + private ISysConfigService configService; + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + if (captcha == null) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); + throw new CaptchaExpireException(); + } + redisCache.deleteObject(verifyKey); + if (!code.equalsIgnoreCase(captcha)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); + throw new CaptchaException(); + } + } + } + +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/SmsCodeService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/SmsCodeService.java new file mode 100644 index 0000000..60c5617 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/SmsCodeService.java @@ -0,0 +1,82 @@ +package org.lingniu.idp.service.core; + +import lombok.extern.slf4j.Slf4j; +import org.lingniu.idp.common.redis.RedisCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +public class SmsCodeService { + + @Autowired + private RedisCache redisCache; + + private static final String SMS_CODE_PREFIX = "sms:code:"; + private static final long SMS_CODE_EXPIRE = 300; // 5分钟 + + /** + * 生成并发送验证码 + */ + public void sendSmsCode(String phone) { + // 1. 生成验证码 + String code = generateCode(); + String key = SMS_CODE_PREFIX + phone; + // 2. 存储到Redis + redisCache.setCacheObject( + key, + code + ); + redisCache.expire(key,SMS_CODE_EXPIRE); + + // 3. 发送短信(这里使用腾讯云示例,可根据实际情况替换) + sendSms(phone, code); + + log.info("向手机号 {} 发送验证码: {}", phone, code); + } + + /** + * 验证短信验证码 + */ + public boolean verifyCode(String phone, String code) { + String key = SMS_CODE_PREFIX + phone; + String storedCode = (String) redisCache.getCacheObject(key); + + if (storedCode != null && storedCode.equals(code)) { + // 验证成功后删除验证码 + redisCache.deleteObject(key); + return true; + } + return false; + } + + private String generateCode() { + // 生成6位随机数字 + Random random = new Random(); + return String.format("%06d", random.nextInt(999999)); + } + + private void sendSms(String phone, String code) { + try { + // 腾讯云短信发送示例 +// Credential cred = new Credential("secretId", "secretKey"); +// SmsClient client = new SmsClient(cred, "ap-guangzhou"); +// +// SendSmsRequest req = new SendSmsRequest(); +// req.setPhoneNumberSet(new String[]{"+86" + phone}); +// req.setSmsSdkAppId("your_app_id"); +// req.setSignName("your_sign_name"); +// req.setTemplateId("your_template_id"); +// req.setTemplateParamSet(new String[]{code, "5"}); +// +// SendSmsResponse resp = client.SendSms(req); +// log.info("短信发送响应: {}", SendSmsResponse.toJsonString(resp)); + } catch (Exception e) { + log.error("短信发送失败", e); + } + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/SysPermissionService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/SysPermissionService.java new file mode 100644 index 0000000..d8a9c8a --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/SysPermissionService.java @@ -0,0 +1,200 @@ +package org.lingniu.idp.service.core; + +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.constant.UserConstants; +import org.lingniu.idp.model.entity.SysDept; +import org.lingniu.idp.model.entity.SysRole; +import org.lingniu.idp.model.entity.SysUser; +import org.lingniu.idp.model.vo.DataPermission; +import org.lingniu.idp.service.ISysDeptService; +import org.lingniu.idp.service.ISysMenuService; +import org.lingniu.idp.service.ISysRoleService; +import org.lingniu.idp.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 用户权限处理 + * + * @author portal + */ +@Component +public class SysPermissionService +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysMenuService menuService; + @Autowired + private ISysDeptService deptService; + + /** + * 获取角色数据权限 + * + * @param user 用户信息 + * @return 角色权限信息 + */ + public Set getRolePermission(SysUser user) + { + Set roles = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + roles.add(Constants.SUPER_ADMIN); + } + else + { + roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); + } + return roles; + } + + /** + * 获取数据权限 + * @return + * 1=所有数据权限,2=自定义数据权限,3=本部门及以下数据权限,4=本部门数据权限,5=仅本人数据权限 + **/ + public DataPermission getDataPermission(SysUser user) { + // 1. 管理员看所有 + if (user.isAdmin()) { + return DataPermission.builder().allowAll(true).build(); + } + + // 2. 获取用户角色并处理 + List sysRoles = roleService.selectRolesByUserId(user.getUserId()); + ProcessResult processResult = processRoles(sysRoles); + + // 3. 如果没有有效范围值,返回默认仅看自己 + return processResult.minScopeExcluding2() + .map(scope -> buildDataPermissionByScope(scope, processResult, user)) + .orElse(DataPermission.builder().onlySelf(true).build()); + } + + public record ProcessResult(Optional minScopeExcluding2, boolean hasScope2) {} + + private ProcessResult processRoles(List sysRoles) { + boolean hasScope2 = sysRoles.stream() + .anyMatch(role -> "2".equals(role.getDataScope())); + + Optional minScopeExcluding2 = sysRoles.stream() + .map(SysRole::getDataScope) + .filter(scope -> !"2".equals(scope)) + .map(this::parseScopeToInt) + .flatMap(Optional::stream) + .min(Integer::compareTo); + + return new ProcessResult(minScopeExcluding2, hasScope2); + } + + private DataPermission buildDataPermissionByScope(Integer scope, ProcessResult processResult, SysUser user) { + return switch (scope) { + case 1 -> DataPermission.builder().allowAll(true).build(); + case 3 -> buildDataPermissionForScope3(processResult, user); + case 4 -> buildDataPermissionForScope4(processResult, user); + case 5 -> buildDataPermissionForScope5(processResult, user); + default -> DataPermission.builder().onlySelf(true).build(); + }; + } + + private DataPermission buildDataPermissionForScope3(ProcessResult processResult, SysUser user) { + Set deptSet = new HashSet<>(); + Set areaSet = new HashSet<>(); + + // 添加本部门及以下部门 + user.getDeptList().forEach(dept -> { + List children = deptService.selectChildrenDeptById(dept.getDeptId()); + addDeptAndAreaInfo(children, deptSet, areaSet); + }); + + // 添加自定义部门(如果有) + if (processResult.hasScope2()) { + List customDepts = deptService.selectDeptListByUserRole(user.getUserId()); + addDeptAndAreaInfo(customDepts, deptSet, areaSet); + } + + return DataPermission.builder().deptList(deptSet).areas(areaSet).build(); + } + + private DataPermission buildDataPermissionForScope4(ProcessResult processResult, SysUser user) { + return buildDataPermissionWithCustomDeptsOnly(processResult, user, false); + } + + private DataPermission buildDataPermissionForScope5(ProcessResult processResult, SysUser user) { + return buildDataPermissionWithCustomDeptsOnly(processResult, user, true); + } + + private DataPermission buildDataPermissionWithCustomDeptsOnly(ProcessResult processResult, SysUser user, boolean onlySelf) { + Set deptSet = new HashSet<>(); + Set areaSet = new HashSet<>(); + + if (processResult.hasScope2()) { + List customDepts = deptService.selectDeptListByUserRole(user.getUserId()); + addDeptAndAreaInfo(customDepts, deptSet, areaSet); + } + + return DataPermission.builder() + .deptList(deptSet) + .areas(areaSet) + .onlySelf(onlySelf) + .build(); + } + + private void addDeptAndAreaInfo(List deptList, Set deptSet, Set areaSet) { + deptList.forEach(dept -> { + deptSet.add(String.valueOf(dept.getDeptId())); + if (dept.getAreaId() != null) { + areaSet.add(String.valueOf(dept.getAreaId())); + } + }); + } + + private Optional parseScopeToInt(String scope) { + try { + return Optional.of(Integer.parseInt(scope)); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + /** + * 获取菜单数据权限 + * + * @param user 用户信息 + * @return 菜单权限信息 + */ + public Set getMenuPermission(SysUser user) + { + Set perms = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + perms.add(Constants.ALL_PERMISSION); + } + else + { + List roles = user.getRoles(); + if (!CollectionUtils.isEmpty(roles)) + { + // 多角色设置permissions属性,以便数据权限匹配权限 + for (SysRole role : roles) + { + if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && !role.isAdmin()) + { + Set rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId()); + role.setPermissions(rolePerms); + perms.addAll(rolePerms); + } + } + } + else + { + perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); + } + } + return perms; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/UserDetailsServiceImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/UserDetailsServiceImpl.java new file mode 100644 index 0000000..c81b048 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/UserDetailsServiceImpl.java @@ -0,0 +1,62 @@ +package org.lingniu.idp.service.core; + +import org.lingniu.idp.model.entity.SysUser; +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.enums.UserStatus; +import org.lingniu.idp.exception.ServiceException; +import org.lingniu.idp.service.ISysUserService; +import org.lingniu.idp.utils.MessageUtils; +import org.lingniu.idp.utils.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +/** + * 用户验证处理 + * + * @author portal + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService +{ + private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); + + @Autowired + private ISysUserService userService; + + + @Autowired + private SysPermissionService permissionService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException + { + SysUser user = userService.selectUserByUserName(username); + if (StringUtils.isNull(user)) + { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } + else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + log.info("登录用户:{} 已被删除.", username); + throw new ServiceException(MessageUtils.message("user.password.delete")); + } + else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException(MessageUtils.message("user.blocked")); + } + + return createLoginUser(user); + } + + public UserDetails createLoginUser(SysUser user) + { + return new LoginUser(user, permissionService.getMenuPermission(user)); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/IdpTokenService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/IdpTokenService.java new file mode 100644 index 0000000..974c9a8 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/IdpTokenService.java @@ -0,0 +1,202 @@ +package org.lingniu.idp.service.core.login; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.model.vo.AccessTokenInfo; +import org.lingniu.idp.model.vo.RefreshTokenInfo; +import org.lingniu.idp.model.vo.TokenInfo; +import org.lingniu.idp.service.ISysUserService; +import org.lingniu.idp.utils.DateUtils; +import org.lingniu.idp.utils.DeviceFingerprintUtil; +import org.lingniu.idp.utils.ip.IpUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.web.authentication.rememberme.InvalidCookieException; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.UUID; + +@Component +@Slf4j +public class IdpTokenService { + // 令牌有效期(默认30分钟)单位分钟 + @Value("${token.accessToken.expireTime:30}") + private int accessTokenExpireTime; + // 刷新令牌有效期(默认24小时) 单位小时 + @Value("${token.refreshToken.expireTime:24}") + private int refreshTokenExpireTime; + @Value("${token.header:Idp}") + private String tokenHeader; + private final RedisRefreshTokenService refreshTokenService; + + private final RedisAccessTokenService accessTokenService; + private final ISysUserService userService; + + public IdpTokenService(RedisRefreshTokenService refreshTokenService, RedisAccessTokenService accessTokenService, ISysUserService userService) { + this.refreshTokenService = refreshTokenService; + this.accessTokenService = accessTokenService; + this.userService = userService; + } + + public TokenInfo createToken(HttpServletRequest request, LoginUser loginUser){ + String accessToken = UUID.randomUUID().toString().replace("-", ""); + String refreshToken = UUID.randomUUID().toString().replace("-", ""); + String ip = IpUtils.getIpAddr(request); + String deviceId = DeviceFingerprintUtil.generateFingerprint(request); + String userAgent = request.getHeader("User-Agent"); + Instant issuedAt = Instant.now(); + Instant accessExpiresAt = issuedAt.plus(accessTokenExpireTime,ChronoUnit.SECONDS); + Instant refreshExpiresAt = issuedAt.plus(refreshTokenExpireTime, ChronoUnit.HOURS); + AccessTokenInfo accessTokenInfo = AccessTokenInfo.builder() + .tokenValue(accessToken) + .userAgent(userAgent) + .userId(loginUser.getUser().getUserId().toString()) + .username(loginUser.getUsername()) + .issuedAt(issuedAt) + .expiresAt(accessExpiresAt) + .deviceId(deviceId) + .ipAddress(ip) + .refreshTokenId(refreshToken) + .build(); + RefreshTokenInfo refreshTokenInfo = RefreshTokenInfo.builder() + .tokenValue(refreshToken) + .userId(loginUser.getUser().getUserId().toString()) + .accessToken(accessToken) + .username(loginUser.getUsername()) + .deviceId(deviceId) + .createdAt(issuedAt) + .lastUsedAt(refreshExpiresAt) + .accessTokenCount(1) + .usageCount(0) + .ipAddress(ip) + .build(); + return new TokenInfo(accessTokenInfo,refreshTokenInfo); + } + public void storeTokenInfo(TokenInfo tokenInfo){ + accessTokenService.storeAccessToken(tokenInfo.getAccessTokenInfo()); + + refreshTokenService.storeRefreshToken(tokenInfo.getRefreshTokenInfo()); + } + public void setAccessTokenHeader(HttpServletResponse response,String accessToken){ + response.addHeader(tokenHeader,accessToken); + + } + + public void setRefreshTokenCookie(HttpServletResponse response, TokenInfo tokenInfo) { + String refreshToken = tokenInfo.getRefreshTokenInfo().getTokenValue(); +// Cookie cookie = new Cookie("refresh_token", refreshToken); +// cookie.setHttpOnly(true); +// cookie.setSecure(true); // 生产环境设为true +// cookie.setPath("/"); +// cookie.setMaxAge(refreshTokenExpireTime * 60 * 60); +// cookie.setDomain(".lingniu.com"); // 设置域名 + + // 添加SameSite属性 + response.addHeader("Set-Cookie", + String.format("idp_refresh_token=%s; HttpOnly; Secure; Path=/; Max-Age=%d; SameSite=Strict", + refreshToken, refreshTokenExpireTime * 60 * 60)); + } + public String getCookieRefreshToken(HttpServletRequest request){ + Cookie[] cookies = request.getCookies(); + return Arrays.stream(cookies) + .filter(cookie -> "idp_refresh_token".equals(cookie.getName())) + .map(Cookie::getValue) + .findFirst() + .orElse(null); + } + public boolean validateAccessToken(HttpServletRequest request){ + String accessToken = request.getHeader(tokenHeader); + return accessTokenService.validateAccessToken(accessToken); + } + + public AccessTokenInfo getAccessTokenInfo(HttpServletRequest request){ + String accessToken = request.getHeader(tokenHeader); + return accessTokenService.getAccessTokenInfo(accessToken); + } + public RefreshTokenInfo getRefreshTokenInfo(HttpServletRequest request){ + String accessToken = getCookieRefreshToken(request); + return refreshTokenService.getRefreshTokenInfo(accessToken); + } + + public AccessTokenInfo refreshToken(HttpServletRequest request,HttpServletResponse response) throws IOException { + String accessToken = request.getHeader(tokenHeader); + String cookieRefreshToken = getCookieRefreshToken(request); + RefreshTokenInfo refreshTokenInfo = refreshTokenService.getRefreshTokenInfo(cookieRefreshToken); + if(refreshTokenInfo == null || !refreshTokenInfo.isValid()){ + log.info("refresh token is expire"); + return null; + } + + if(refreshTokenInfo.getAccessToken()!=null && !refreshTokenInfo.getAccessToken().equals(accessToken)){ + log.info("token 已刷新"); + } + String accessTokenNew = UUID.randomUUID().toString().replace("-", ""); + String ip = IpUtils.getIpAddr(request); + String deviceId = DeviceFingerprintUtil.generateFingerprint(request); + String userAgent = request.getHeader("User-Agent"); + Instant issuedAt = Instant.now(); + Instant accessExpiresAt = issuedAt.plusSeconds(accessTokenExpireTime * 60L); + AccessTokenInfo accessTokenInfo = AccessTokenInfo.builder() + .tokenValue(accessTokenNew) + .userAgent(userAgent) + .userId(refreshTokenInfo.getUserId()) + .username(refreshTokenInfo.getUsername()) + .issuedAt(issuedAt) + .expiresAt(accessExpiresAt) + .deviceId(deviceId) + .ipAddress(ip) + .refreshTokenId(refreshTokenInfo.getTokenValue()) + .build(); + accessTokenService.storeAccessToken(accessTokenInfo); + + refreshTokenInfo.incrementUsage(); + refreshTokenInfo.incrementAccessTokenUsage(accessTokenNew); + refreshTokenService.updateRefreshToken(refreshTokenInfo); + setAccessTokenHeader(response,accessTokenNew); + + return accessTokenInfo; + } + public void revokeToken(HttpServletRequest request,HttpServletResponse response){ + String accessToken = request.getHeader(tokenHeader); + String cookieRefreshToken = getCookieRefreshToken(request); + AccessTokenInfo accessTokenInfo = accessTokenService.getAccessTokenInfo(accessToken); + RefreshTokenInfo refreshTokenInfo = refreshTokenService.getRefreshTokenInfo(cookieRefreshToken); + Instant now = Instant.now(); + if(accessTokenInfo!=null){ + accessTokenInfo.setRevokedAt(now); + accessTokenInfo.setRevoked(true); + accessTokenService.revokeAccessToken(accessTokenInfo); + } + if(refreshTokenInfo!=null){ + refreshTokenInfo.setRevokedAt(now); + refreshTokenInfo.setRevoked(true); + refreshTokenService.revokeRefreshToken(refreshTokenInfo); + } + clearRefreshTokenCookie(response); + } + + public void clearRefreshTokenCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("idp_refresh_token", null); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(0); + + response.addCookie(cookie); + } + /** + * 记录登录信息 + * + * @param userId 用户ID + */ + public void recordLoginInfo(Long userId) + { + userService.updateLoginInfo(userId, IpUtils.getIpAddr(), DateUtils.getNowDate()); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/LoginService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/LoginService.java new file mode 100644 index 0000000..c4752e0 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/LoginService.java @@ -0,0 +1,177 @@ +package org.lingniu.idp.service.core.login; + +import org.lingniu.idp.common.redis.RedisCache; +import org.lingniu.idp.constant.CacheConstants; +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.constant.UserConstants; +import org.lingniu.idp.exception.user.BlackListException; +import org.lingniu.idp.exception.user.UserNotExistsException; +import org.lingniu.idp.exception.user.UserPasswordNotMatchException; +import org.lingniu.idp.exception.user.UserPasswordRetryLimitExceedException; +import org.lingniu.idp.manager.AsyncManager; +import org.lingniu.idp.manager.factory.AsyncFactory; +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.model.entity.SysUser; +import org.lingniu.idp.model.vo.DataPermission; +import org.lingniu.idp.service.ISysConfigService; +import org.lingniu.idp.service.ISysUserService; +import org.lingniu.idp.service.core.CaptchaService; +import org.lingniu.idp.service.core.SysPermissionService; +import org.lingniu.idp.utils.MessageUtils; +import org.lingniu.idp.utils.SecurityUtils; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.utils.ip.IpUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + + +/** + * 登录校验方法 + * + * @author portal + */ +@Component +public class LoginService +{ + + @Autowired + private RedisCache redisCache; + + @Value(value = "${user.password.maxRetryCount}") + private int maxRetryCount; + + @Value(value = "${user.password.lockTime}") + private int lockTime; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + @Autowired + private CaptchaService captchaService; + + @Autowired + private SysPermissionService permissionService; + /** + * 登录验证 + * + * @param username 用户名 + * @param password 密码 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public LoginUser login(String username, String password, String code, String uuid) + { + // 验证码校验 + captchaService.validateCaptcha(username, code, uuid); + // 登录前置校验 + loginPreCheck(username, password); + // 2. 加载用户 + SysUser sysUser = userService.selectUserByUserName(username); + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + if (!matches(sysUser.getPassword(), password)) + { + retryCount = retryCount + 1; + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new UserPasswordNotMatchException(); + } + else + { + clearLoginRecordCache(username); + } + return new LoginUser(sysUser, permissionService.getMenuPermission(sysUser)); + } + + /** + * 登录前置校验 + * @param username 用户名 + * @param password 用户密码 + */ + public void loginPreCheck(String username, String password) + { + // 用户名或密码为空 错误 + if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"))); + throw new UserNotExistsException(); + } + // 密码如果不在指定范围内 错误 + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // 用户名不在指定范围内 错误 + if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // IP黑名单校验 + String blackStr = configService.selectConfigByKey("sys.login.blackIPList"); + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked"))); + throw new BlackListException(); + } + } + public Map getPermissionInfo(SysUser sysUser){ + // 角色集合 + Set roles = permissionService.getRolePermission(sysUser); + // 权限集合 + Set permissions = permissionService.getMenuPermission(sysUser); + // 数据权限 + DataPermission dataPermission = permissionService.getDataPermission(sysUser); + + Map map = new HashMap<>(); + map.put("roles",roles); + map.put("permissions",permissions); + map.put("dataPermission",dataPermission); + + return map; + } + public SysUser getUserDetail(String userName){ + SysUser sysUser = userService.selectUserByUserName(userName); + + return sysUser; + } + private String getCacheKey(String username) + { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + + public boolean matches(String password, String rawPassword) + { + return SecurityUtils.matchesPassword(rawPassword, password); + } + + public void clearLoginRecordCache(String loginName) + { + if (redisCache.hasKey(getCacheKey(loginName))) + { + redisCache.deleteObject(getCacheKey(loginName)); + } + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/RedisAccessTokenService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/RedisAccessTokenService.java new file mode 100644 index 0000000..a071b04 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/RedisAccessTokenService.java @@ -0,0 +1,84 @@ +package org.lingniu.idp.service.core.login; + +import lombok.extern.slf4j.Slf4j; +import org.lingniu.idp.common.redis.RedisCache; +import org.lingniu.idp.constant.CacheConstants; +import org.lingniu.idp.model.vo.AccessTokenInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Map; + +@Component +@Slf4j +public class RedisAccessTokenService { + + @Autowired + private RedisCache redisCache; + + private final long ACCESS_TOKEN_EXPIRE = 3600; // 1小时 + + /** + * 存储Access Token到Redis + */ + public void storeAccessToken(AccessTokenInfo tokenInfo) { + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, tokenInfo.getTokenValue()); + try { + redisCache.setCacheMap(key,tokenInfo.toMap()); + Instant expiresAt = tokenInfo.getExpiresAt(); + long expire = ACCESS_TOKEN_EXPIRE; + if(expiresAt!=null){ + expire = expiresAt.getEpochSecond() - Instant.now().getEpochSecond(); + } + + redisCache.expire(key,expire); + } catch (Exception e) { + log.error("存储Access Token失败", e); + } + } + + /** + * 验证Access Token + */ + public boolean validateAccessToken(String token) { + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, token); + if(!redisCache.hasKey(key)){ + return false; + } + AccessTokenInfo accessTokenInfo = getAccessTokenInfo(token); + if(accessTokenInfo==null){ + return false; + } + return accessTokenInfo.isValid(); + } + + /** + * 删除Access Token + */ + public boolean removeAccessToken(String token) { + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, token); + return redisCache.deleteObject(key); + } + + /** + * 获取Access Token信息 + */ + public AccessTokenInfo getAccessTokenInfo(String token) { + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, token); + Map cacheMap = redisCache.getCacheMap(key); + if(cacheMap!=null){ + return AccessTokenInfo.fromMap(cacheMap); + } + return null; + } + + /** + * 作废 不删除 + * @param tokenInfo + */ + public void revokeAccessToken(AccessTokenInfo tokenInfo) { + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, tokenInfo.getTokenValue()); + redisCache.setCacheMap(key,tokenInfo.toRevokeMap()); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/RedisRefreshTokenService.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/RedisRefreshTokenService.java new file mode 100644 index 0000000..017a720 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/RedisRefreshTokenService.java @@ -0,0 +1,72 @@ +package org.lingniu.idp.service.core.login; + +import lombok.extern.slf4j.Slf4j; +import org.lingniu.idp.common.redis.RedisCache; +import org.lingniu.idp.constant.CacheConstants; +import org.lingniu.idp.model.vo.RefreshTokenInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Map; + +@Component +@Slf4j +public class RedisRefreshTokenService { + + @Autowired + private RedisCache redisCache; + + private final long REFRESH_TOKEN_EXPIRE = 30 * 24 * 3600L; // 30天 + + /** + * 存储Refresh Token到Redis Hash + */ + public void storeRefreshToken(RefreshTokenInfo tokenInfo) { + String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, tokenInfo.getTokenValue()); + + redisCache.setCacheMap(key,tokenInfo.toMap()); + Instant expiresAt = tokenInfo.getExpiresAt(); + long expire = REFRESH_TOKEN_EXPIRE; + if(expiresAt!=null){ + expire = expiresAt.getEpochSecond() - Instant.now().getEpochSecond(); + } + + redisCache.expire(key,expire); + // 维护用户会话列表 + String userSessionsKey = String.format(CacheConstants.USER_SESSIONS, tokenInfo.getUserId()); + redisCache.setCacheSet(userSessionsKey,tokenInfo.getTokenValue()); + redisCache.expire(userSessionsKey,expire); + } + + /** + * 获取Refresh Token信息 + */ + public RefreshTokenInfo getRefreshTokenInfo(String token) { + String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, token); + Map cacheMap = redisCache.getCacheMap(key); + if(cacheMap!=null){ + return RefreshTokenInfo.fromMap(cacheMap); + } + return null; + } + + /** + * 更新Refresh Token最后使用时间 + */ + public void updateRefreshToken(RefreshTokenInfo tokenInfo) { + String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, tokenInfo.getTokenValue()); + redisCache.setCacheMap(key,tokenInfo.toUpdateMap()); + } + + /** + * 作废 不删除 + * @param tokenInfo + */ + public void revokeRefreshToken(RefreshTokenInfo tokenInfo) { + String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, tokenInfo.getTokenValue()); + String userSessionsKey = String.format(CacheConstants.USER_SESSIONS, tokenInfo.getUserId()); + redisCache.deleteCacheSetValue(userSessionsKey,tokenInfo.getTokenValue()); + redisCache.setCacheMap(key,tokenInfo.toRevokeMap()); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/SecurityEnhancer.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/SecurityEnhancer.java new file mode 100644 index 0000000..ea971d3 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/core/login/SecurityEnhancer.java @@ -0,0 +1,138 @@ +package org.lingniu.idp.service.core.login; + +import lombok.extern.slf4j.Slf4j; +import org.lingniu.idp.common.redis.RedisCache; +import org.lingniu.idp.constant.CacheConstants; +import org.lingniu.idp.constant.Constants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +@Component +@Slf4j +public class SecurityEnhancer { + + @Autowired + private RedisCache redisCache; + + private final int MAX_SESSIONS_PER_USER = 5; + + /** + * 会话管理 - 限制每个用户的最大会话数 + */ + public void enforceSessionLimit(String userId) { + String userSessionsKey = String.format(CacheConstants.USER_SESSIONS, userId); + Set sessions = redisCache.getCacheSet(userSessionsKey); + + if (sessions != null && sessions.size() >= MAX_SESSIONS_PER_USER) { + // 移除最旧的会话 + removeOldestSession(userId, sessions); + } + } + + private void removeOldestSession(String userId, Set sessions) { + String oldestSession = null; + Instant oldestTime = Instant.MAX; + + for (Object sessionToken : sessions) { + String token = (String) sessionToken; + String refreshTokenKey = String.format(CacheConstants.REFRESH_TOKEN_KEY, token); + Map hash = redisCache.getCacheMap(refreshTokenKey); + + if (hash.containsKey("lastUsedAt")) { + Instant lastUsedAt = Instant.parse((String) hash.get("lastUsedAt")); + if (lastUsedAt.isBefore(oldestTime)) { + oldestTime = lastUsedAt; + oldestSession = token; + } + } + } + + if (oldestSession != null) { + revokeToken(oldestSession, "refresh_token"); + log.info("用户{}超出会话限制,移除最旧会话: {}", userId, oldestSession); + } + } + + /** + * Token撤销机制 + */ + public void revokeToken(String token, String tokenType) { + if ("access_token".equals(tokenType)) { + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, token); + redisCache.deleteObject(key); + log.info("撤销Access Token: {}", token); + } else if ("refresh_token".equals(tokenType)) { + String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, token); + + // 获取用户ID,从用户会话列表中移除 + Map hash = redisCache.getCacheMap(key); + if (hash.containsKey("userId")) { + String userId = (String) hash.get("userId"); + String userSessionsKey = String.format(CacheConstants.USER_SESSIONS, userId); + redisCache.deleteCacheSetValue(userSessionsKey,token); + } + + redisCache.deleteObject(key); + log.info("撤销Refresh Token: {}", token); + } + } + + /** + * 设备指纹验证 + */ + public boolean validateDeviceFingerprint(String userId, String deviceId, String deviceFingerprint) { + if (deviceId == null || deviceFingerprint == null) { + return true; // 如果设备信息为空,允许通过(可能是首次登录) + } + + String deviceKey = String.format("device_fingerprint:%s:%s", userId, deviceId); + String storedFingerprint = redisCache.getCacheObject(deviceKey); + + if (storedFingerprint == null) { + // 首次使用该设备,存储指纹 + redisCache.setCacheObject(deviceKey,deviceFingerprint); + redisCache.expire(deviceKey,30, TimeUnit.DAYS); + return true; + } + + return deviceFingerprint.equals(storedFingerprint); + } + + /** + * 强制用户下线 + */ + public void forceLogout(String userId) { + String userSessionsKey = String.format(CacheConstants.USER_SESSIONS, userId); + Set sessions = redisCache.getCacheSet(userSessionsKey); + + if (sessions != null) { + for (Object sessionToken : sessions) { + revokeToken((String) sessionToken, "refresh_token"); + } + } + + log.info("强制用户{}下线,清除所有会话", userId); + } + + /** + * 检查token是否在黑名单中 + */ + public boolean isTokenBlacklisted(String token) { + String key = "token_blacklist:" + token; + return Boolean.TRUE.equals(redisCache.hasKey(key)); + } + + /** + * 将token加入黑名单 + */ + public void blacklistToken(String token, long expireSeconds) { + String key = "token_blacklist:" + token; + redisCache.setCacheObject(key,"1"); + redisCache.expire(key,expireSeconds,TimeUnit.SECONDS); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysConfigServiceImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..89b6372 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,233 @@ +package org.lingniu.idp.service.impl; + +import java.util.Collection; +import java.util.List; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.lingniu.idp.annotation.DataSource; +import org.lingniu.idp.constant.CacheConstants; +import org.lingniu.idp.constant.UserConstants; +import org.lingniu.idp.common.redis.RedisCache; +import org.lingniu.idp.utils.text.Convert; +import org.lingniu.idp.enums.DataSourceType; +import org.lingniu.idp.exception.ServiceException; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.model.entity.SysConfig; +import org.lingniu.idp.mapper.SysConfigMapper; +import org.lingniu.idp.service.ISysConfigService; + +/** + * 参数配置 服务层实现 + * + * @author portal + */ +@Service +public class SysConfigServiceImpl implements ISysConfigService +{ + @Autowired + private SysConfigMapper configMapper; + + @Autowired + private RedisCache redisCache; + + /** + * 项目启动时,初始化参数到缓存 + */ + @PostConstruct + public void init() + { + loadingConfigCache(); + } + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + @Override + @DataSource(DataSourceType.MASTER) + public SysConfig selectConfigById(Long configId) + { + SysConfig config = new SysConfig(); + config.setConfigId(configId); + return configMapper.selectConfig(config); + } + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数key + * @return 参数键值 + */ + @Override + public String selectConfigByKey(String configKey) + { + String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey))); + if (StringUtils.isNotEmpty(configValue)) + { + return configValue; + } + SysConfig config = new SysConfig(); + config.setConfigKey(configKey); + SysConfig retConfig = configMapper.selectConfig(config); + if (StringUtils.isNotNull(retConfig)) + { + redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue()); + return retConfig.getConfigValue(); + } + return StringUtils.EMPTY; + } + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + @Override + public boolean selectCaptchaEnabled() + { + String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled"); + if (StringUtils.isEmpty(captchaEnabled)) + { + return true; + } + return Convert.toBool(captchaEnabled); + } + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + @Override + public List selectConfigList(SysConfig config) + { + return configMapper.selectConfigList(config); + } + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int insertConfig(SysConfig config) + { + int row = configMapper.insertConfig(config); + if (row > 0) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int updateConfig(SysConfig config) + { + SysConfig temp = configMapper.selectConfigById(config.getConfigId()); + if (!StringUtils.equals(temp.getConfigKey(), config.getConfigKey())) + { + redisCache.deleteObject(getCacheKey(temp.getConfigKey())); + } + + int row = configMapper.updateConfig(config); + if (row > 0) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + @Override + public void deleteConfigByIds(Long[] configIds) + { + for (Long configId : configIds) + { + SysConfig config = selectConfigById(configId); + if (StringUtils.equals(UserConstants.YES, config.getConfigType())) + { + throw new ServiceException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey())); + } + configMapper.deleteConfigById(configId); + redisCache.deleteObject(getCacheKey(config.getConfigKey())); + } + } + + /** + * 加载参数缓存数据 + */ + @Override + public void loadingConfigCache() + { + List configsList = configMapper.selectConfigList(new SysConfig()); + for (SysConfig config : configsList) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + } + + /** + * 清空参数缓存数据 + */ + @Override + public void clearConfigCache() + { + Collection keys = redisCache.keys(CacheConstants.SYS_CONFIG_KEY + "*"); + redisCache.deleteObject(keys); + } + + /** + * 重置参数缓存数据 + */ + @Override + public void resetConfigCache() + { + clearConfigCache(); + loadingConfigCache(); + } + + /** + * 校验参数键名是否唯一 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public boolean checkConfigKeyUnique(SysConfig config) + { + Long configId = StringUtils.isNull(config.getConfigId()) ? -1L : config.getConfigId(); + SysConfig info = configMapper.checkConfigKeyUnique(config.getConfigKey()); + if (StringUtils.isNotNull(info) && info.getConfigId().longValue() != configId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + private String getCacheKey(String configKey) + { + return CacheConstants.SYS_CONFIG_KEY + configKey; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysDeptServiceImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysDeptServiceImpl.java new file mode 100644 index 0000000..0011cbd --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,345 @@ +package org.lingniu.idp.service.impl; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.lingniu.idp.annotation.DataScope; +import org.lingniu.idp.constant.UserConstants; +import org.lingniu.idp.model.base.TreeSelect; +import org.lingniu.idp.model.entity.SysDept; +import org.lingniu.idp.model.entity.SysRole; +import org.lingniu.idp.model.entity.SysUser; +import org.lingniu.idp.utils.text.Convert; +import org.lingniu.idp.exception.ServiceException; +import org.lingniu.idp.utils.SecurityUtils; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.utils.spring.SpringUtils; +import org.lingniu.idp.mapper.SysDeptMapper; +import org.lingniu.idp.mapper.SysRoleMapper; +import org.lingniu.idp.service.ISysDeptService; + +/** + * 部门管理 服务实现 + * + * @author portal + */ +@Service +public class SysDeptServiceImpl implements ISysDeptService +{ + @Autowired + private SysDeptMapper deptMapper; + + @Autowired + private SysRoleMapper roleMapper; + + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + @Override + @DataScope(deptAlias = "d") + public List selectDeptList(SysDept dept) + { + return deptMapper.selectDeptList(dept); + } + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + @Override + public List selectDeptTreeList(SysDept dept) + { + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + return buildDeptTreeSelect(depts); + } + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + @Override + public List buildDeptTree(List depts) + { + List returnList = new ArrayList(); + List tempList = depts.stream().map(SysDept::getDeptId).collect(Collectors.toList()); + for (SysDept dept : depts) + { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(dept.getParentId())) + { + recursionFn(depts, dept); + returnList.add(dept); + } + } + if (returnList.isEmpty()) + { + returnList = depts; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + @Override + public List buildDeptTreeSelect(List depts) + { + List deptTrees = buildDeptTree(depts); + return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + @Override + public List selectDeptListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return deptMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly()); + } + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + @Override + public SysDept selectDeptById(Long deptId) + { + return deptMapper.selectDeptById(deptId); + } + @Override + public List selectDeptListByUserRole(Long userId){ + return deptMapper.selectDeptListByUserRole(userId); + } + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + @Override + public int selectNormalChildrenDeptById(Long deptId) + { + return deptMapper.selectNormalChildrenDeptById(deptId); + } + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public boolean hasChildByDeptId(Long deptId) + { + int result = deptMapper.hasChildByDeptId(deptId); + return result > 0; + } + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + @Override + public boolean checkDeptExistUser(Long deptId) + { + int result = deptMapper.checkDeptExistUser(deptId); + return result > 0; + } + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public boolean checkDeptNameUnique(SysDept dept) + { + Long deptId = StringUtils.isNull(dept.getDeptId()) ? -1L : dept.getDeptId(); + SysDept info = deptMapper.checkDeptNameUnique(dept.getDeptName(), dept.getParentId()); + if (StringUtils.isNotNull(info) && info.getDeptId().longValue() != deptId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + @Override + public void checkDeptDataScope(Long deptId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId()) && StringUtils.isNotNull(deptId)) + { + SysDept dept = new SysDept(); + dept.setDeptId(deptId); + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + if (StringUtils.isEmpty(depts)) + { + throw new ServiceException("没有权限访问部门数据!"); + } + } + } + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int insertDept(SysDept dept) + { + SysDept info = deptMapper.selectDeptById(dept.getParentId()); + // 如果父节点不为正常状态,则不允许新增子节点 + if (!UserConstants.DEPT_NORMAL.equals(info.getStatus())) + { + throw new ServiceException("部门停用,不允许新增"); + } + dept.setAncestors(info.getAncestors() + "," + dept.getParentId()); + return deptMapper.insertDept(dept); + } + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int updateDept(SysDept dept) + { + SysDept newParentDept = deptMapper.selectDeptById(dept.getParentId()); + SysDept oldDept = deptMapper.selectDeptById(dept.getDeptId()); + if (StringUtils.isNotNull(newParentDept) && StringUtils.isNotNull(oldDept)) + { + String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId(); + String oldAncestors = oldDept.getAncestors(); + dept.setAncestors(newAncestors); + updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors); + } + int result = deptMapper.updateDept(dept); + if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors()) + && !StringUtils.equals("0", dept.getAncestors())) + { + // 如果该部门是启用状态,则启用该部门的所有上级部门 + updateParentDeptStatusNormal(dept); + } + return result; + } + + /** + * 修改该部门的父级部门状态 + * + * @param dept 当前部门 + */ + private void updateParentDeptStatusNormal(SysDept dept) + { + String ancestors = dept.getAncestors(); + Long[] deptIds = Convert.toLongArray(ancestors); + deptMapper.updateDeptStatusNormal(deptIds); + } + + /** + * 修改子元素关系 + * + * @param deptId 被修改的部门ID + * @param newAncestors 新的父ID集合 + * @param oldAncestors 旧的父ID集合 + */ + public void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors) + { + List children = deptMapper.selectChildrenDeptById(deptId); + for (SysDept child : children) + { + child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); + } + if (children.size() > 0) + { + deptMapper.updateDeptChildren(children); + } + } + public List selectChildrenDeptById(Long deptId){ + return deptMapper.selectChildrenDeptById(deptId); + } + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public int deleteDeptById(Long deptId) + { + return deptMapper.deleteDeptById(deptId); + } + + /** + * 递归列表 + */ + private void recursionFn(List list, SysDept t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysDept tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysDept t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysDept n = (SysDept) it.next(); + if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getDeptId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysDept t) + { + return getChildList(list, t).size() > 0; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysLogininforServiceImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysLogininforServiceImpl.java new file mode 100644 index 0000000..5a1c585 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysLogininforServiceImpl.java @@ -0,0 +1,65 @@ +package org.lingniu.idp.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.lingniu.idp.model.entity.SysLogininfor; +import org.lingniu.idp.mapper.SysLogininforMapper; +import org.lingniu.idp.service.ISysLogininforService; + +/** + * 系统访问日志情况信息 服务层处理 + * + * @author portal + */ +@Service +public class SysLogininforServiceImpl implements ISysLogininforService +{ + + @Autowired + private SysLogininforMapper logininforMapper; + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + @Override + public void insertLogininfor(SysLogininfor logininfor) + { + logininforMapper.insertLogininfor(logininfor); + } + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + @Override + public List selectLogininforList(SysLogininfor logininfor) + { + return logininforMapper.selectLogininforList(logininfor); + } + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + @Override + public int deleteLogininforByIds(Long[] infoIds) + { + return logininforMapper.deleteLogininforByIds(infoIds); + } + + /** + * 清空系统登录日志 + */ + @Override + public void cleanLogininfor() + { + logininforMapper.cleanLogininfor(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysMenuServiceImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..70894c3 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,543 @@ +package org.lingniu.idp.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.constant.UserConstants; +import org.lingniu.idp.model.base.TreeSelect; +import org.lingniu.idp.model.entity.SysMenu; +import org.lingniu.idp.model.entity.SysRole; +import org.lingniu.idp.model.entity.SysUser; +import org.lingniu.idp.utils.SecurityUtils; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.model.vo.MetaVo; +import org.lingniu.idp.model.vo.RouterVo; +import org.lingniu.idp.mapper.SysMenuMapper; +import org.lingniu.idp.mapper.SysRoleMapper; +import org.lingniu.idp.mapper.SysRoleMenuMapper; +import org.lingniu.idp.service.ISysMenuService; + +/** + * 菜单 业务层处理 + * + * @author portal + */ +@Service +public class SysMenuServiceImpl implements ISysMenuService +{ + public static final String PREMISSION_STRING = "perms[\"{0}\"]"; + + @Autowired + private SysMenuMapper menuMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + @Override + public List selectMenuList(Long userId) + { + return selectMenuList(new SysMenu(), userId); + } + + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + @Override + public List selectMenuList(SysMenu menu, Long userId) + { + List menuList = null; + // 管理员显示所有菜单信息 + if (SysUser.isAdmin(userId)) + { + menuList = menuMapper.selectMenuList(menu); + } + else + { + menu.getParams().put("userId", userId); + menuList = menuMapper.selectMenuListByUserId(menu); + } + return menuList; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByUserId(Long userId) + { + List perms = menuMapper.selectMenuPermsByUserId(userId); + Set permsSet = new HashSet<>(); + for (String perm : perms) + { + if (StringUtils.isNotEmpty(perm)) + { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByRoleId(Long roleId) + { + List perms = menuMapper.selectMenuPermsByRoleId(roleId); + Set permsSet = new HashSet<>(); + for (String perm : perms) + { + if (StringUtils.isNotEmpty(perm)) + { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户名称 + * @return 菜单列表 + */ + @Override + public List selectMenuTreeByUserId(Long userId) + { + List menus = null; + if (SecurityUtils.isAdmin(userId)) + { + menus = menuMapper.selectMenuTreeAll(); + } + else + { + menus = menuMapper.selectMenuTreeByUserId(userId); + } + return getChildPerms(menus, 0); + } + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + @Override + public List selectMenuListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return menuMapper.selectMenuListByRoleId(roleId, role.isMenuCheckStrictly()); + } + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + @Override + public List buildMenus(List menus) + { + List routers = new LinkedList(); + for (SysMenu menu : menus) + { + RouterVo router = new RouterVo(); + router.setHidden("1".equals(menu.getVisible())); + router.setName(getRouteName(menu)); + router.setPath(getRouterPath(menu)); + router.setComponent(getComponent(menu)); + router.setQuery(menu.getQuery()); + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + List cMenus = menu.getChildren(); + if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType())) + { + router.setAlwaysShow(true); + router.setRedirect("noRedirect"); + router.setChildren(buildMenus(cMenus)); + } + else if (isMenuFrame(menu)) + { + router.setMeta(null); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + children.setPath(menu.getPath()); + children.setComponent(menu.getComponent()); + children.setName(getRouteName(menu.getRouteName(), menu.getPath())); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + children.setQuery(menu.getQuery()); + childrenList.add(children); + router.setChildren(childrenList); + } + else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) + { + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon())); + router.setPath("/"); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + String routerPath = innerLinkReplaceEach(menu.getPath()); + children.setPath(routerPath); + children.setComponent(UserConstants.INNER_LINK); + children.setName(getRouteName(menu.getRouteName(), routerPath)); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath())); + childrenList.add(children); + router.setChildren(childrenList); + } + routers.add(router); + } + return routers; + } + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + @Override + public List buildMenuTree(List menus) + { + List returnList = new ArrayList(); + List tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList()); + for (Iterator iterator = menus.iterator(); iterator.hasNext();) + { + SysMenu menu = (SysMenu) iterator.next(); + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(menu.getParentId())) + { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) + { + returnList = menus; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + @Override + public List buildMenuTreeSelect(List menus) + { + List menuTrees = buildMenuTree(menus); + return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + @Override + public SysMenu selectMenuById(Long menuId) + { + return menuMapper.selectMenuById(menuId); + } + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean hasChildByMenuId(Long menuId) + { + int result = menuMapper.hasChildByMenuId(menuId); + return result > 0; + } + + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean checkMenuExistRole(Long menuId) + { + int result = roleMenuMapper.checkMenuExistRole(menuId); + return result > 0; + } + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int insertMenu(SysMenu menu) + { + return menuMapper.insertMenu(menu); + } + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int updateMenu(SysMenu menu) + { + return menuMapper.updateMenu(menu); + } + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public int deleteMenuById(Long menuId) + { + return menuMapper.deleteMenuById(menuId); + } + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public boolean checkMenuNameUnique(SysMenu menu) + { + Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + SysMenu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId()); + if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 获取路由名称 + * + * @param menu 菜单信息 + * @return 路由名称 + */ + public String getRouteName(SysMenu menu) + { + // 非外链并且是一级目录(类型为目录) + if (isMenuFrame(menu)) + { + return StringUtils.EMPTY; + } + return getRouteName(menu.getRouteName(), menu.getPath()); + } + + /** + * 获取路由名称,如没有配置路由名称则取路由地址 + * + * @param name 路由名称 + * @param path 路由地址 + * @return 路由名称(驼峰格式) + */ + public String getRouteName(String name, String path) + { + String routerName = StringUtils.isNotEmpty(name) ? name : path; + return StringUtils.capitalize(routerName); + } + + /** + * 获取路由地址 + * + * @param menu 菜单信息 + * @return 路由地址 + */ + public String getRouterPath(SysMenu menu) + { + String routerPath = menu.getPath(); + // 内链打开外网方式 + if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) + { + routerPath = innerLinkReplaceEach(routerPath); + } + // 非外链并且是一级目录(类型为目录) + if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType()) + && UserConstants.NO_FRAME.equals(menu.getIsFrame())) + { + routerPath = "/" + menu.getPath(); + } + // 非外链并且是一级目录(类型为菜单) + else if (isMenuFrame(menu)) + { + routerPath = "/"; + } + return routerPath; + } + + /** + * 获取组件信息 + * + * @param menu 菜单信息 + * @return 组件信息 + */ + public String getComponent(SysMenu menu) + { + String component = UserConstants.LAYOUT; + if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) + { + component = menu.getComponent(); + } + else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu)) + { + component = UserConstants.INNER_LINK; + } + else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) + { + component = UserConstants.PARENT_VIEW; + } + return component; + } + + /** + * 是否为菜单内部跳转 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isMenuFrame(SysMenu menu) + { + return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType()) + && menu.getIsFrame().equals(UserConstants.NO_FRAME); + } + + /** + * 是否为内链组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isInnerLink(SysMenu menu) + { + return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath()); + } + + /** + * 是否为parent_view组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isParentView(SysMenu menu) + { + return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()); + } + + /** + * 根据父节点的ID获取所有子节点 + * + * @param list 分类表 + * @param parentId 传入的父节点ID + * @return String + */ + public List getChildPerms(List list, int parentId) + { + List returnList = new ArrayList(); + for (Iterator iterator = list.iterator(); iterator.hasNext();) + { + SysMenu t = (SysMenu) iterator.next(); + // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 + if (t.getParentId() == parentId) + { + recursionFn(list, t); + returnList.add(t); + } + } + return returnList; + } + + /** + * 递归列表 + * + * @param list 分类表 + * @param t 子节点 + */ + private void recursionFn(List list, SysMenu t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysMenu tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysMenu t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysMenu n = (SysMenu) it.next(); + if (n.getParentId().longValue() == t.getMenuId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysMenu t) + { + return getChildList(list, t).size() > 0; + } + + /** + * 内链域名特殊字符替换 + * + * @return 替换后的内链域名 + */ + public String innerLinkReplaceEach(String path) + { + return StringUtils.replaceEach(path, new String[] { Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":" }, + new String[] { "", "", "", "/", "/" }); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysOperLogServiceImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysOperLogServiceImpl.java new file mode 100644 index 0000000..007f8f6 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysOperLogServiceImpl.java @@ -0,0 +1,76 @@ +package org.lingniu.idp.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.lingniu.idp.model.entity.SysOperLog; +import org.lingniu.idp.mapper.SysOperLogMapper; +import org.lingniu.idp.service.ISysOperLogService; + +/** + * 操作日志 服务层处理 + * + * @author portal + */ +@Service +public class SysOperLogServiceImpl implements ISysOperLogService +{ + @Autowired + private SysOperLogMapper operLogMapper; + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + @Override + public void insertOperlog(SysOperLog operLog) + { + operLogMapper.insertOperlog(operLog); + } + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + @Override + public List selectOperLogList(SysOperLog operLog) + { + return operLogMapper.selectOperLogList(operLog); + } + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + @Override + public int deleteOperLogByIds(Long[] operIds) + { + return operLogMapper.deleteOperLogByIds(operIds); + } + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + @Override + public SysOperLog selectOperLogById(Long operId) + { + return operLogMapper.selectOperLogById(operId); + } + + /** + * 清空操作日志 + */ + @Override + public void cleanOperLog() + { + operLogMapper.cleanOperLog(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysPostServiceImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysPostServiceImpl.java new file mode 100644 index 0000000..da2a661 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysPostServiceImpl.java @@ -0,0 +1,178 @@ +package org.lingniu.idp.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.lingniu.idp.constant.UserConstants; +import org.lingniu.idp.exception.ServiceException; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.model.entity.SysPost; +import org.lingniu.idp.mapper.SysPostMapper; +import org.lingniu.idp.mapper.SysUserPostMapper; +import org.lingniu.idp.service.ISysPostService; + +/** + * 岗位信息 服务层处理 + * + * @author portal + */ +@Service +public class SysPostServiceImpl implements ISysPostService +{ + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位信息集合 + */ + @Override + public List selectPostList(SysPost post) + { + return postMapper.selectPostList(post); + } + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + @Override + public List selectPostAll() + { + return postMapper.selectPostAll(); + } + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + @Override + public SysPost selectPostById(Long postId) + { + return postMapper.selectPostById(postId); + } + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + @Override + public List selectPostListByUserId(Long userId) + { + return postMapper.selectPostListByUserId(userId); + } + + /** + * 校验岗位名称是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostNameUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostNameUnique(post.getPostName()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验岗位编码是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostCodeUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostCodeUnique(post.getPostCode()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int countUserPostById(Long postId) + { + return userPostMapper.countUserPostById(postId); + } + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int deletePostById(Long postId) + { + return postMapper.deletePostById(postId); + } + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + @Override + public int deletePostByIds(Long[] postIds) + { + for (Long postId : postIds) + { + SysPost post = selectPostById(postId); + if (countUserPostById(postId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", post.getPostName())); + } + } + return postMapper.deletePostByIds(postIds); + } + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int insertPost(SysPost post) + { + return postMapper.insertPost(post); + } + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int updatePost(SysPost post) + { + return postMapper.updatePost(post); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysRoleServiceImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..11e6a49 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,427 @@ +package org.lingniu.idp.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.lingniu.idp.annotation.DataScope; +import org.lingniu.idp.constant.UserConstants; +import org.lingniu.idp.model.entity.SysRole; +import org.lingniu.idp.model.entity.SysUser; +import org.lingniu.idp.exception.ServiceException; +import org.lingniu.idp.utils.SecurityUtils; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.utils.spring.SpringUtils; +import org.lingniu.idp.model.entity.SysRoleDept; +import org.lingniu.idp.model.entity.SysRoleMenu; +import org.lingniu.idp.model.entity.SysUserRole; +import org.lingniu.idp.mapper.SysRoleDeptMapper; +import org.lingniu.idp.mapper.SysRoleMapper; +import org.lingniu.idp.mapper.SysRoleMenuMapper; +import org.lingniu.idp.mapper.SysUserRoleMapper; +import org.lingniu.idp.service.ISysRoleService; + +/** + * 角色 业务层处理 + * + * @author portal + */ +@Service +public class SysRoleServiceImpl implements ISysRoleService +{ + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysRoleDeptMapper roleDeptMapper; + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + @Override + @DataScope(deptAlias = "d") + public List selectRoleList(SysRole role) + { + return roleMapper.selectRoleList(role); + } + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + @Override + public List selectRolesByUserId(Long userId) + { + List userRoles = roleMapper.selectRolePermissionByUserId(userId); + List roles = selectRoleAll(); + for (SysRole role : roles) + { + for (SysRole userRole : userRoles) + { + if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) + { + role.setFlag(true); + break; + } + } + } + return roles; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectRolePermissionByUserId(Long userId) + { + List perms = roleMapper.selectRolePermissionByUserId(userId); + Set permsSet = new HashSet<>(); + for (SysRole perm : perms) + { + if (StringUtils.isNotNull(perm)) + { + permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(","))); + } + } + return permsSet; + } + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + @Override + public List selectRoleAll() + { + return SpringUtils.getAopProxy(this).selectRoleList(new SysRole()); + } + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + @Override + public List selectRoleListByUserId(Long userId) + { + return roleMapper.selectRoleListByUserId(userId); + } + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + @Override + public SysRole selectRoleById(Long roleId) + { + return roleMapper.selectRoleById(roleId); + } + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleNameUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleNameUnique(role.getRoleName()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleKeyUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleKeyUnique(role.getRoleKey()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + @Override + public void checkRoleAllowed(SysRole role) + { + if (StringUtils.isNotNull(role.getRoleId()) && role.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员角色"); + } + } + + /** + * 校验角色是否有数据权限 + * + * @param roleIds 角色id + */ + @Override + public void checkRoleDataScope(Long... roleIds) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + for (Long roleId : roleIds) + { + SysRole role = new SysRole(); + role.setRoleId(roleId); + List roles = SpringUtils.getAopProxy(this).selectRoleList(role); + if (StringUtils.isEmpty(roles)) + { + throw new ServiceException("没有权限访问角色数据!"); + } + } + } + } + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + public int countUserRoleByRoleId(Long roleId) + { + return userRoleMapper.countUserRoleByRoleId(roleId); + } + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int insertRole(SysRole role) + { + // 新增角色信息 + roleMapper.insertRole(role); + return insertRoleMenu(role); + } + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int updateRole(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId()); + return insertRoleMenu(role); + } + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public int updateRoleStatus(SysRole role) + { + return roleMapper.updateRole(role); + } + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int authDataScope(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId()); + // 新增角色和部门信息(数据权限) + return insertRoleDept(role); + } + + /** + * 新增角色菜单信息 + * + * @param role 角色对象 + */ + public int insertRoleMenu(SysRole role) + { + int rows = 1; + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long menuId : role.getMenuIds()) + { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(role.getRoleId()); + rm.setMenuId(menuId); + list.add(rm); + } + if (list.size() > 0) + { + rows = roleMenuMapper.batchRoleMenu(list); + } + return rows; + } + + /** + * 新增角色部门信息(数据权限) + * + * @param role 角色对象 + */ + public int insertRoleDept(SysRole role) + { + int rows = 1; + // 新增角色与部门(数据权限)管理 + List list = new ArrayList(); + for (Long deptId : role.getDeptIds()) + { + SysRoleDept rd = new SysRoleDept(); + rd.setRoleId(role.getRoleId()); + rd.setDeptId(deptId); + list.add(rd); + } + if (list.size() > 0) + { + rows = roleDeptMapper.batchRoleDept(list); + } + return rows; + } + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleById(Long roleId) + { + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(roleId); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(roleId); + return roleMapper.deleteRoleById(roleId); + } + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleByIds(Long[] roleIds) + { + for (Long roleId : roleIds) + { + checkRoleAllowed(new SysRole(roleId)); + checkRoleDataScope(roleId); + SysRole role = selectRoleById(roleId); + if (countUserRoleByRoleId(roleId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName())); + } + } + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenu(roleIds); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDept(roleIds); + return roleMapper.deleteRoleByIds(roleIds); + } + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + @Override + public int deleteAuthUser(SysUserRole userRole) + { + return userRoleMapper.deleteUserRoleInfo(userRole); + } + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + @Override + public int deleteAuthUsers(Long roleId, Long[] userIds) + { + return userRoleMapper.deleteUserRoleInfos(roleId, userIds); + } + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要授权的用户数据ID + * @return 结果 + */ + @Override + public int insertAuthUsers(Long roleId, Long[] userIds) + { + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long userId : userIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + return userRoleMapper.batchUserRole(list); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysUserOnlineServiceImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysUserOnlineServiceImpl.java new file mode 100644 index 0000000..385e3c7 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysUserOnlineServiceImpl.java @@ -0,0 +1,96 @@ +package org.lingniu.idp.service.impl; + +import org.springframework.stereotype.Service; +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.model.entity.SysUserOnline; +import org.lingniu.idp.service.ISysUserOnlineService; + +/** + * 在线用户 服务层处理 + * + * @author portal + */ +@Service +public class SysUserOnlineServiceImpl implements ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user) + { + if (StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + @Override + public SysUserOnline loginUserToUserOnline(LoginUser user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUser())) + { + return null; + } + SysUserOnline sysUserOnline = new SysUserOnline(); + sysUserOnline.setTokenId(user.getToken()); + sysUserOnline.setUserName(user.getUsername()); + sysUserOnline.setIpaddr(user.getIpaddr()); + sysUserOnline.setLoginLocation(user.getLoginLocation()); + sysUserOnline.setBrowser(user.getBrowser()); + sysUserOnline.setOs(user.getOs()); + sysUserOnline.setLoginTime(user.getLoginTime()); + if (StringUtils.isNotNull(user.getUser().getDept())) + { + sysUserOnline.setDeptName(user.getUser().getDept().getDeptName()); + } + return sysUserOnline; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysUserServiceImpl.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..d5923a5 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/service/impl/SysUserServiceImpl.java @@ -0,0 +1,566 @@ +package org.lingniu.idp.service.impl; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.validation.Validator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.lingniu.idp.annotation.DataScope; +import org.lingniu.idp.constant.UserConstants; +import org.lingniu.idp.model.entity.SysRole; +import org.lingniu.idp.model.entity.SysUser; +import org.lingniu.idp.exception.ServiceException; +import org.lingniu.idp.utils.SecurityUtils; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.utils.bean.BeanValidators; +import org.lingniu.idp.utils.spring.SpringUtils; +import org.lingniu.idp.model.entity.SysPost; +import org.lingniu.idp.model.entity.SysUserPost; +import org.lingniu.idp.model.entity.SysUserRole; +import org.lingniu.idp.mapper.SysPostMapper; +import org.lingniu.idp.mapper.SysRoleMapper; +import org.lingniu.idp.mapper.SysUserMapper; +import org.lingniu.idp.mapper.SysUserPostMapper; +import org.lingniu.idp.mapper.SysUserRoleMapper; +import org.lingniu.idp.service.ISysConfigService; +import org.lingniu.idp.service.ISysDeptService; +import org.lingniu.idp.service.ISysUserService; + +/** + * 用户 业务层处理 + * + * @author portal + */ +@Service +public class SysUserServiceImpl implements ISysUserService +{ + private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class); + + @Autowired + private SysUserMapper userMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + private ISysDeptService deptService; + + @Autowired + protected Validator validator; + + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUserList(SysUser user) + { + return userMapper.selectUserList(user); + } + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectAllocatedList(SysUser user) + { + return userMapper.selectAllocatedList(user); + } + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUnallocatedList(SysUser user) + { + return userMapper.selectUnallocatedList(user); + } + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + @Override + public SysUser selectUserByUserName(String userName) + { + return userMapper.selectUserByUserName(userName); + } + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + @Override + public SysUser selectUserById(Long userId) + { + return userMapper.selectUserById(userId); + } + + /** + * 查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserRoleGroup(String userName) + { + List list = roleMapper.selectRolesByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(",")); + } + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserPostGroup(String userName) + { + List list = postMapper.selectPostsByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysPost::getPostName).collect(Collectors.joining(",")); + } + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean checkUserNameUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkUserNameUnique(user.getUserName()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkPhoneUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkEmailUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkEmailUnique(user.getEmail()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + @Override + public void checkUserAllowed(SysUser user) + { + if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员用户"); + } + } + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + @Override + public void checkUserDataScope(Long userId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + SysUser user = new SysUser(); + user.setUserId(userId); + List users = SpringUtils.getAopProxy(this).selectUserList(user); + if (StringUtils.isEmpty(users)) + { + throw new ServiceException("没有权限访问用户数据!"); + } + } + } + + /** + * 新增保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int insertUser(SysUser user) + { + // 新增用户信息 + int rows = userMapper.insertUser(user); + // 新增用户岗位关联 + insertUserPost(user); + // 新增用户与角色管理 + insertUserRole(user); + return rows; + } + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean registerUser(SysUser user) + { + return userMapper.insertUser(user) > 0; + } + + /** + * 修改保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int updateUser(SysUser user) + { + Long userId = user.getUserId(); + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 新增用户与角色管理 + insertUserRole(user); + // 删除用户与岗位关联 + userPostMapper.deleteUserPostByUserId(userId); + // 新增用户与岗位管理 + insertUserPost(user); + return userMapper.updateUser(user); + } + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + @Override + @Transactional + public void insertUserAuth(Long userId, Long[] roleIds) + { + userRoleMapper.deleteUserRoleByUserId(userId); + insertUserRole(userId, roleIds); + } + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserStatus(SysUser user) + { + return userMapper.updateUserStatus(user.getUserId(), user.getStatus()); + } + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserProfile(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + @Override + public boolean updateUserAvatar(Long userId, String avatar) + { + return userMapper.updateUserAvatar(userId, avatar) > 0; + } + + /** + * 更新用户登录信息(IP和登录时间) + * + * @param userId 用户ID + * @param loginIp 登录IP地址 + * @param loginDate 登录时间 + * @return 结果 + */ + public void updateLoginInfo(Long userId, String loginIp, Date loginDate) + { + userMapper.updateLoginInfo(userId, loginIp, loginDate); + } + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int resetPwd(SysUser user) + { + return userMapper.resetUserPwd(user.getUserId(), user.getPassword()); + } + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + @Override + public int resetUserPwd(Long userId, String password) + { + return userMapper.resetUserPwd(userId, password); + } + + /** + * 新增用户角色信息 + * + * @param user 用户对象 + */ + public void insertUserRole(SysUser user) + { + this.insertUserRole(user.getUserId(), user.getRoleIds()); + } + + /** + * 新增用户岗位信息 + * + * @param user 用户对象 + */ + public void insertUserPost(SysUser user) + { + Long[] posts = user.getPostIds(); + if (StringUtils.isNotEmpty(posts)) + { + // 新增用户与岗位管理 + List list = new ArrayList(posts.length); + for (Long postId : posts) + { + SysUserPost up = new SysUserPost(); + up.setUserId(user.getUserId()); + up.setPostId(postId); + list.add(up); + } + userPostMapper.batchUserPost(list); + } + } + + /** + * 新增用户角色信息 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserRole(Long userId, Long[] roleIds) + { + if (StringUtils.isNotEmpty(roleIds)) + { + // 新增用户与角色管理 + List list = new ArrayList(roleIds.length); + for (Long roleId : roleIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + userRoleMapper.batchUserRole(list); + } + } + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserById(Long userId) + { + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 删除用户与岗位表 + userPostMapper.deleteUserPostByUserId(userId); + return userMapper.deleteUserById(userId); + } + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserByIds(Long[] userIds) + { + for (Long userId : userIds) + { + checkUserAllowed(new SysUser(userId)); + checkUserDataScope(userId); + } + // 删除用户与角色关联 + userRoleMapper.deleteUserRole(userIds); + // 删除用户与岗位关联 + userPostMapper.deleteUserPost(userIds); + return userMapper.deleteUserByIds(userIds); + } + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + @Override + public String importUser(List userList, Boolean isUpdateSupport, String operName) + { + if (StringUtils.isNull(userList) || userList.size() == 0) + { + throw new ServiceException("导入用户数据不能为空!"); + } + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + for (SysUser user : userList) + { + try + { + // 验证是否存在这个用户 + SysUser u = userMapper.selectUserByUserName(user.getUserName()); + if (StringUtils.isNull(u)) + { + BeanValidators.validateWithException(validator, user); + deptService.checkDeptDataScope(user.getDeptId()); + String password = configService.selectConfigByKey("sys.user.initPassword"); + user.setPassword(SecurityUtils.encryptPassword(password)); + user.setCreateBy(operName); + userMapper.insertUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 导入成功"); + } + else if (isUpdateSupport) + { + BeanValidators.validateWithException(validator, user); + checkUserAllowed(u); + checkUserDataScope(u.getUserId()); + deptService.checkDeptDataScope(user.getDeptId()); + user.setUserId(u.getUserId()); + user.setDeptId(u.getDeptId()); + user.setUpdateBy(operName); + userMapper.updateUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 更新成功"); + } + else + { + failureNum++; + failureMsg.append("
" + failureNum + "、账号 " + user.getUserName() + " 已存在"); + } + } + catch (Exception e) + { + failureNum++; + String msg = "
" + failureNum + "、账号 " + user.getUserName() + " 导入失败:"; + failureMsg.append(msg + e.getMessage()); + log.error(msg, e); + } + } + if (failureNum > 0) + { + failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); + throw new ServiceException(failureMsg.toString()); + } + else + { + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + } + return successMsg.toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ClientCredentialUtil.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ClientCredentialUtil.java new file mode 100644 index 0000000..f195d68 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ClientCredentialUtil.java @@ -0,0 +1,92 @@ +package org.lingniu.idp.utils; + +import com.alibaba.fastjson2.JSON; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import java.util.Base64; +import java.util.Map; +import java.util.UUID; + +/** + * Client凭证生成和验证工具 + */ +public class ClientCredentialUtil { + + private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); + + /** + * 生成随机Client Secret(Base64编码) + * @param length 字节长度,推荐32字节(256位) + * @return 随机生成的Client Secret + */ + public static String generateRandomSecret(int length) { + byte[] randomBytes = new byte[length]; + new java.security.SecureRandom().nextBytes(randomBytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); + } + + /** + * 使用BCrypt加密Client Secret + * @param rawSecret 原始明文Secret + * @return BCrypt加密后的Secret + */ + public static String encodeWithBCrypt(String rawSecret) { + return PASSWORD_ENCODER.encode(rawSecret); + } + + /** + * 生成Spring Security格式的加密Secret + * @param rawSecret 原始明文Secret + * @return {bcrypt}加密格式 + */ + public static String encodeWithBCryptPrefix(String rawSecret) { + return "{bcrypt}" + PASSWORD_ENCODER.encode(rawSecret); + } + + /** + * 验证Client Secret + * @param rawSecret 原始明文Secret + * @param encodedSecret 已加密的Secret(可带或不带前缀) + * @return 验证结果 + */ + public static boolean verifySecret(String rawSecret, String encodedSecret) { + // 移除前缀(如果有) + if (encodedSecret.startsWith("{bcrypt}")) { + encodedSecret = encodedSecret.substring(8); + } else if (encodedSecret.startsWith("{noop}")) { + // 如果是明文存储 + return rawSecret.equals(encodedSecret.substring(6)); + } + return PASSWORD_ENCODER.matches(rawSecret, encodedSecret); + } + + /** + * 生成Client ID(UUID格式) + * @return 生成的Client ID + */ + public static String generateClientId() { + return UUID.randomUUID().toString().replace("-", "").substring(0, 24); + } + + /** + * 生成完整的Client凭证 + * @return 包含Client ID、原始Secret和加密Secret的Map + */ + public static java.util.Map generateClientCredentials() { + String clientId = generateClientId(); + String rawSecret = generateRandomSecret(32); + String encodedSecret = encodeWithBCryptPrefix(rawSecret); + + java.util.Map credentials = new java.util.HashMap<>(); + credentials.put("clientId", clientId); + credentials.put("rawSecret", rawSecret); + credentials.put("encodedSecret", encodedSecret); + + return credentials; + } + + public static void main(String[] args) { + Map stringStringMap = generateClientCredentials(); + System.out.println(JSON.toJSONString(stringStringMap)); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/DateUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/DateUtils.java new file mode 100644 index 0000000..f55a947 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/DateUtils.java @@ -0,0 +1,188 @@ +package org.lingniu.idp.utils; + +import org.apache.commons.lang3.time.DateFormatUtils; + +import java.lang.management.ManagementFactory; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.*; +import java.util.Date; + +/** + * 时间工具类 + * + * @author portal + */ +public class DateUtils extends org.apache.commons.lang3.time.DateUtils +{ + public static String YYYY = "yyyy"; + + public static String YYYY_MM = "yyyy-MM"; + + public static String YYYY_MM_DD = "yyyy-MM-dd"; + + public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; + + public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + + private static String[] parsePatterns = { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", + "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", + "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"}; + + /** + * 获取当前Date型日期 + * + * @return Date() 当前日期 + */ + public static Date getNowDate() + { + return new Date(); + } + + /** + * 获取当前日期, 默认格式为yyyy-MM-dd + * + * @return String + */ + public static String getDate() + { + return dateTimeNow(YYYY_MM_DD); + } + + public static final String getTime() + { + return dateTimeNow(YYYY_MM_DD_HH_MM_SS); + } + + public static final String dateTimeNow() + { + return dateTimeNow(YYYYMMDDHHMMSS); + } + + public static final String dateTimeNow(final String format) + { + return parseDateToStr(format, new Date()); + } + + public static final String dateTime(final Date date) + { + return parseDateToStr(YYYY_MM_DD, date); + } + + public static final String parseDateToStr(final String format, final Date date) + { + return new SimpleDateFormat(format).format(date); + } + + public static final Date dateTime(final String format, final String ts) + { + try + { + return new SimpleDateFormat(format).parse(ts); + } + catch (ParseException e) + { + throw new RuntimeException(e); + } + } + + /** + * 日期路径 即年/月/日 如2018/08/08 + */ + public static final String datePath() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyy/MM/dd"); + } + + /** + * 日期路径 即年/月/日 如20180808 + */ + public static final String dateTime() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyyMMdd"); + } + + /** + * 日期型字符串转化为日期 格式 + */ + public static Date parseDate(Object str) + { + if (str == null) + { + return null; + } + try + { + return parseDate(str.toString(), parsePatterns); + } + catch (ParseException e) + { + return null; + } + } + + /** + * 获取服务器启动时间 + */ + public static Date getServerStartDate() + { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + /** + * 计算相差天数 + */ + public static int differentDaysByMillisecond(Date date1, Date date2) + { + return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24))); + } + + /** + * 计算时间差 + * + * @param endDate 最后时间 + * @param startTime 开始时间 + * @return 时间差(天/小时/分钟) + */ + public static String timeDistance(Date endDate, Date startTime) + { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + // long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - startTime.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + // long sec = diff % nd % nh % nm / ns; + return day + "天" + hour + "小时" + min + "分钟"; + } + + /** + * 增加 LocalDateTime ==> Date + */ + public static Date toDate(LocalDateTime temporalAccessor) + { + ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + /** + * 增加 LocalDate ==> Date + */ + public static Date toDate(LocalDate temporalAccessor) + { + LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0)); + ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/DesensitizedUtil.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/DesensitizedUtil.java new file mode 100644 index 0000000..957365e --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/DesensitizedUtil.java @@ -0,0 +1,49 @@ +package org.lingniu.idp.utils; + +/** + * 脱敏工具类 + * + * @author portal + */ +public class DesensitizedUtil +{ + /** + * 密码的全部字符都用*代替,比如:****** + * + * @param password 密码 + * @return 脱敏后的密码 + */ + public static String password(String password) + { + if (StringUtils.isBlank(password)) + { + return StringUtils.EMPTY; + } + return StringUtils.repeat('*', password.length()); + } + + /** + * 车牌中间用*代替,如果是错误的车牌,不处理 + * + * @param carLicense 完整的车牌号 + * @return 脱敏后的车牌 + */ + public static String carLicense(String carLicense) + { + if (StringUtils.isBlank(carLicense)) + { + return StringUtils.EMPTY; + } + // 普通车牌 + if (carLicense.length() == 7) + { + carLicense = StringUtils.hide(carLicense, 3, 6); + } + else if (carLicense.length() == 8) + { + // 新能源车牌 + carLicense = StringUtils.hide(carLicense, 3, 7); + } + return carLicense; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/DeviceFingerprintUtil.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/DeviceFingerprintUtil.java new file mode 100644 index 0000000..5359e51 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/DeviceFingerprintUtil.java @@ -0,0 +1,32 @@ +package org.lingniu.idp.utils; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.util.DigestUtils; + +// 设备指纹工具 +public class DeviceFingerprintUtil { + + public static String generateFingerprint(HttpServletRequest request) { + String userAgent = request.getHeader("User-Agent"); + String acceptLanguage = request.getHeader("Accept-Language"); + String acceptEncoding = request.getHeader("Accept-Encoding"); + String ipAddress = getClientIp(request); + + String fingerprintSource = userAgent + "|" + acceptLanguage + "|" + acceptEncoding + "|" + ipAddress; + return DigestUtils.md5DigestAsHex(fingerprintSource.getBytes()); + } + + private static String getClientIp(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip.split(",")[0].trim(); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/EscapeUtil.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/EscapeUtil.java new file mode 100644 index 0000000..284903d --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/EscapeUtil.java @@ -0,0 +1,166 @@ +package org.lingniu.idp.utils; + + +/** + * 转义和反转义工具类 + * + * @author portal + */ +public class EscapeUtil +{ + public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)"; + + private static final char[][] TEXT = new char[64][]; + + static + { + for (int i = 0; i < 64; i++) + { + TEXT[i] = new char[] { (char) i }; + } + + // special HTML characters + TEXT['\''] = "'".toCharArray(); // 单引号 + TEXT['"'] = """.toCharArray(); // 双引号 + TEXT['&'] = "&".toCharArray(); // &符 + TEXT['<'] = "<".toCharArray(); // 小于号 + TEXT['>'] = ">".toCharArray(); // 大于号 + } + + /** + * 转义文本中的HTML字符为安全的字符 + * + * @param text 被转义的文本 + * @return 转义后的文本 + */ + public static String escape(String text) + { + return encode(text); + } + + /** + * 还原被转义的HTML特殊字符 + * + * @param content 包含转义符的HTML内容 + * @return 转换后的字符串 + */ + public static String unescape(String content) + { + return decode(content); + } + + /** + * 清除所有HTML标签,但是不删除标签内的内容 + * + * @param content 文本 + * @return 清除标签后的文本 + */ + public static String clean(String content) + { + return new HTMLFilter().filter(content); + } + + /** + * Escape编码 + * + * @param text 被编码的文本 + * @return 编码后的字符 + */ + private static String encode(String text) + { + if (StringUtils.isEmpty(text)) + { + return StringUtils.EMPTY; + } + + final StringBuilder tmp = new StringBuilder(text.length() * 6); + char c; + for (int i = 0; i < text.length(); i++) + { + c = text.charAt(i); + if (c < 256) + { + tmp.append("%"); + if (c < 16) + { + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + else + { + tmp.append("%u"); + if (c <= 0xfff) + { + // issue#I49JU8@Gitee + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + } + return tmp.toString(); + } + + /** + * Escape解码 + * + * @param content 被转义的内容 + * @return 解码后的字符串 + */ + public static String decode(String content) + { + if (StringUtils.isEmpty(content)) + { + return content; + } + + StringBuilder tmp = new StringBuilder(content.length()); + int lastPos = 0, pos = 0; + char ch; + while (lastPos < content.length()) + { + pos = content.indexOf("%", lastPos); + if (pos == lastPos) + { + if (content.charAt(pos + 1) == 'u') + { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } + else + { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } + } + else + { + if (pos == -1) + { + tmp.append(content.substring(lastPos)); + lastPos = content.length(); + } + else + { + tmp.append(content.substring(lastPos, pos)); + lastPos = pos; + } + } + } + return tmp.toString(); + } + + public static void main(String[] args) + { + String html = ""; + String escape = EscapeUtil.escape(html); + // String html = "ipt>alert(\"XSS\")ipt>"; + // String html = "<123"; + // String html = "123>"; + System.out.println("clean: " + EscapeUtil.clean(html)); + System.out.println("escape: " + escape); + System.out.println("unescape: " + EscapeUtil.unescape(escape)); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/HTMLFilter.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/HTMLFilter.java new file mode 100644 index 0000000..22b9ff2 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/HTMLFilter.java @@ -0,0 +1,566 @@ +package org.lingniu.idp.utils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML过滤器,用于去除XSS漏洞隐患。 + * + * @author portal + */ +public final class HTMLFilter +{ + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap<>(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" + * becomes " text "). If set to false, unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() + { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[] { "img" }; + vNeedClosingTags = new String[] { "a", "b", "strong", "i", "em" }; + vDisallowed = new String[] {}; + vAllowedProtocols = new String[] { "http", "mailto", "https" }; // no ftp. + vProtocolAtts = new String[] { "src", "href" }; + vRemoveBlanks = new String[] { "a", "b", "strong", "i", "em" }; + vAllowedEntities = new String[] { "amp", "gt", "lt", "quot" }; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilter(final Map conf) + { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() + { + vTagCounts.clear(); + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) + { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) + { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) + { + reset(); + String s = input; + + s = escapeComments(s); + + s = balanceHTML(s); + + s = checkTags(s); + + s = processRemoveBlanks(s); + + // s = validateEntities(s); + + return s; + } + + public boolean isAlwaysMakeTags() + { + return alwaysMakeTags; + } + + public boolean isStripComments() + { + return stripComment; + } + + private String escapeComments(final String s) + { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) + { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) + { + if (alwaysMakeTags) + { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + // 不追加结束标签 + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } + else + { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) + { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) + { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + final StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) + { + for (int ii = 0; ii < vTagCounts.get(key); ii++) + { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) + { + String result = s; + for (String tag : vRemoveBlanks) + { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) + { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) + { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) + { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) + { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) + { + if (!inArray(name, vSelfClosingTags)) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) + { + final StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) + { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) + { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) + { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) + { + if (inArray(paramName, vProtocolAtts)) + { + paramValue = processParamProtocol(paramValue); + } + params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\""); + } + } + + if (inArray(name, vSelfClosingTags)) + { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) + { + ending = ""; + } + + if (ending == null || ending.length() < 1) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } + else + { + vTagCounts.put(name, 1); + } + } + else + { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } + else + { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) + { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) + { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) + { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) + { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) + { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) + { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) + { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) + { + if (encodeQuotes) + { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two) + m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); + } + m.appendTail(buf); + return buf.toString(); + } + else + { + return s; + } + } + + private String checkEntity(final String preamble, final String term) + { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) + { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) + { + for (String item : array) + { + if (item != null && item.equals(s)) + { + return true; + } + } + return false; + } + + private boolean allowed(final String name) + { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) + { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/JsonUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/JsonUtils.java new file mode 100644 index 0000000..dd73993 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/JsonUtils.java @@ -0,0 +1,46 @@ +package org.lingniu.idp.utils; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.stereotype.Component; + +@Component +public class JsonUtils { + private static final ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } + + public static String toJson(Object obj) { + try { + return objectMapper.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new RuntimeException("JSON序列化失败", e); + } + } + + public static T fromJson(String json, Class clazz) { + try { + return objectMapper.readValue(json, clazz); + } catch (JsonProcessingException e) { + throw new RuntimeException("JSON反序列化失败", e); + } + } + + public static T fromJson(String json, TypeReference typeReference) { + try { + return objectMapper.readValue(json, typeReference); + } catch (JsonProcessingException e) { + throw new RuntimeException("JSON反序列化失败", e); + } + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/LogUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/LogUtils.java new file mode 100644 index 0000000..571808a --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/LogUtils.java @@ -0,0 +1,18 @@ +package org.lingniu.idp.utils; + +/** + * 处理并记录日志文件 + * + * @author portal + */ +public class LogUtils +{ + public static String getBlock(Object msg) + { + if (msg == null) + { + msg = ""; + } + return "[" + msg.toString() + "]"; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/MessageUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/MessageUtils.java new file mode 100644 index 0000000..3156c6d --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/MessageUtils.java @@ -0,0 +1,26 @@ +package org.lingniu.idp.utils; + +import org.lingniu.idp.utils.spring.SpringUtils; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; + +/** + * 获取i18n资源文件 + * + * @author portal + */ +public class MessageUtils +{ + /** + * 根据消息键和参数 获取消息 委托给spring messageSource + * + * @param code 消息键 + * @param args 参数 + * @return 获取国际化翻译值 + */ + public static String message(String code, Object... args) + { + MessageSource messageSource = SpringUtils.getBean(MessageSource.class); + return messageSource.getMessage(code, args, LocaleContextHolder.getLocale()); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/SecurityUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/SecurityUtils.java new file mode 100644 index 0000000..73abefa --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/SecurityUtils.java @@ -0,0 +1,179 @@ +package org.lingniu.idp.utils; + +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.model.entity.SysRole; +import org.lingniu.idp.model.dto.LoginUser; +import org.lingniu.idp.exception.ServiceException; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.util.PatternMatchUtils; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 安全服务工具类 + * + * @author portal + */ +public class SecurityUtils +{ + + /** + * 用户ID + **/ + public static Long getUserId() + { + try + { + return getLoginUser().getUserId(); + } + catch (Exception e) + { + throw new ServiceException("获取用户ID异常", HttpStatus.UNAUTHORIZED.value()); + } + } + + /** + * 获取部门ID + **/ + public static Long getDeptId() + { + try + { + return getLoginUser().getDeptId(); + } + catch (Exception e) + { + throw new ServiceException("获取部门ID异常", HttpStatus.UNAUTHORIZED.value()); + } + } + + /** + * 获取用户账户 + **/ + public static String getUsername() + { + try + { + return getLoginUser().getUsername(); + } + catch (Exception e) + { + throw new ServiceException("获取用户账户异常", HttpStatus.UNAUTHORIZED.value()); + } + } + + /** + * 获取用户 + **/ + public static LoginUser getLoginUser() + { + try + { + return (LoginUser) getAuthentication().getPrincipal(); + } + catch (Exception e) + { + throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED.value()); + } + } + + /** + * 获取Authentication + */ + public static Authentication getAuthentication() + { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } + + /** + * 判断密码是否相同 + * + * @param rawPassword 真实密码 + * @param encodedPassword 加密后字符 + * @return 结果 + */ + public static boolean matchesPassword(String rawPassword, String encodedPassword) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 是否为管理员 + * + * @param userId 用户ID + * @return 结果 + */ + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } + + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public static boolean hasPermi(String permission) + { + return hasPermi(getLoginUser().getPermissions(), permission); + } + + /** + * 判断是否包含权限 + * + * @param authorities 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public static boolean hasPermi(Collection authorities, String permission) + { + return authorities.stream().filter(StringUtils::hasText) + .anyMatch(x -> Constants.ALL_PERMISSION.equals(x) || PatternMatchUtils.simpleMatch(x, permission)); + } + + /** + * 验证用户是否拥有某个角色 + * + * @param role 角色标识 + * @return 用户是否具备某角色 + */ + public static boolean hasRole(String role) + { + List roleList = getLoginUser().getUser().getRoles(); + Collection roles = roleList.stream().map(SysRole::getRoleKey).collect(Collectors.toSet()); + return hasRole(roles, role); + } + + /** + * 判断是否包含角色 + * + * @param roles 角色列表 + * @param role 角色 + * @return 用户是否具备某角色权限 + */ + public static boolean hasRole(Collection roles, String role) + { + return roles.stream().filter(StringUtils::hasText) + .anyMatch(x -> Constants.SUPER_ADMIN.equals(x) || PatternMatchUtils.simpleMatch(x, role)); + } + +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ServletUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ServletUtils.java new file mode 100644 index 0000000..3408bc7 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ServletUtils.java @@ -0,0 +1,219 @@ +package org.lingniu.idp.utils; + +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.utils.text.Convert; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * 客户端工具类 + * + * @author portal + */ +public class ServletUtils +{ + /** + * 获取String参数 + */ + public static String getParameter(String name) + { + return getRequest().getParameter(name); + } + + /** + * 获取String参数 + */ + public static String getParameter(String name, String defaultValue) + { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name) + { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name, Integer defaultValue) + { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name) + { + return Convert.toBool(getRequest().getParameter(name)); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name, Boolean defaultValue) + { + return Convert.toBool(getRequest().getParameter(name), defaultValue); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParams(ServletRequest request) + { + final Map map = request.getParameterMap(); + return Collections.unmodifiableMap(map); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParamMap(ServletRequest request) + { + Map params = new HashMap<>(); + for (Map.Entry entry : getParams(request).entrySet()) + { + params.put(entry.getKey(), StringUtils.join(entry.getValue(), ",")); + } + return params; + } + + /** + * 获取request + */ + public static HttpServletRequest getRequest() + { + return getRequestAttributes().getRequest(); + } + + /** + * 获取response + */ + public static HttpServletResponse getResponse() + { + return getRequestAttributes().getResponse(); + } + + /** + * 获取session + */ + public static HttpSession getSession() + { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() + { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + */ + public static void renderString(HttpServletResponse response, String string) + { + try + { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + /** + * 是否是Ajax异步请求 + * + * @param request + */ + public static boolean isAjaxRequest(HttpServletRequest request) + { + String accept = request.getHeader("accept"); + if (accept != null && accept.contains("application/json")) + { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) + { + return true; + } + + String uri = request.getRequestURI(); + if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) + { + return true; + } + + String ajax = request.getParameter("__ajax"); + return StringUtils.inStringIgnoreCase(ajax, "json", "xml"); + } + + /** + * 内容编码 + * + * @param str 内容 + * @return 编码后的内容 + */ + public static String urlEncode(String str) + { + try + { + return URLEncoder.encode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } + + /** + * 内容解码 + * + * @param str 内容 + * @return 解码后的内容 + */ + public static String urlDecode(String str) + { + try + { + return URLDecoder.decode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/StringUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/StringUtils.java new file mode 100644 index 0000000..4142d8d --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/StringUtils.java @@ -0,0 +1,718 @@ +package org.lingniu.idp.utils; + +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.utils.text.StrFormatter; +import org.springframework.util.AntPathMatcher; + +import java.util.*; + +/** + * 字符串工具类 + * + * @author portal + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils +{ + /** 空字符串 */ + private static final String NULLSTR = ""; + + /** 下划线 */ + private static final char SEPARATOR = '_'; + + /** 星号 */ + private static final char ASTERISK = '*'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) + { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) + { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) + { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + ** @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) + { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) + { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) + { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) + { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) + { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) + { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) + { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) + { + return !isNull(object); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) + { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) + { + return (str == null ? "" : str.trim()); + } + + /** + * 替换指定字符串的指定区间内字符为"*" + * + * @param str 字符串 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @return 替换后的字符串 + */ + public static String hide(CharSequence str, int startInclude, int endExclude) + { + if (isEmpty(str)) + { + return NULLSTR; + } + final int strLength = str.length(); + if (startInclude > strLength) + { + return NULLSTR; + } + if (endExclude > strLength) + { + endExclude = strLength; + } + if (startInclude > endExclude) + { + // 如果起始位置大于结束位置,不替换 + return NULLSTR; + } + final char[] chars = new char[strLength]; + for (int i = 0; i < strLength; i++) + { + if (i >= startInclude && i < endExclude) + { + chars[i] = ASTERISK; + } + else + { + chars[i] = str.charAt(i); + } + } + return new String(chars); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) + { + if (str == null) + { + return NULLSTR; + } + + if (start < 0) + { + start = str.length() + start; + } + + if (start < 0) + { + start = 0; + } + if (start > str.length()) + { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) + { + if (str == null) + { + return NULLSTR; + } + + if (end < 0) + { + end = str.length() + end; + } + if (start < 0) + { + start = str.length() + start; + } + + if (end > str.length()) + { + end = str.length(); + } + + if (start > end) + { + return NULLSTR; + } + + if (start < 0) + { + start = 0; + } + if (end < 0) + { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 在字符串中查找第一个出现的 `open` 和最后一个出现的 `close` 之间的子字符串 + * + * @param str 要截取的字符串 + * @param open 起始字符串 + * @param close 结束字符串 + * @return 截取结果 + */ + public static String substringBetweenLast(final String str, final String open, final String close) + { + if (isEmpty(str) || isEmpty(open) || isEmpty(close)) + { + return NULLSTR; + } + final int start = str.indexOf(open); + if (start != INDEX_NOT_FOUND) + { + final int end = str.lastIndexOf(close); + if (end != INDEX_NOT_FOUND) + { + return str.substring(start + open.length(), end); + } + } + return NULLSTR; + } + + /** + * 判断是否为空,并且不是空白字符 + * + * @param str 要判断的value + * @return 结果 + */ + public static boolean hasText(String str) + { + return (str != null && !str.isEmpty() && containsText(str)); + } + + private static boolean containsText(CharSequence str) + { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) + { + if (!Character.isWhitespace(str.charAt(i))) + { + return true; + } + } + return false; + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) + { + if (isEmpty(params) || isEmpty(template)) + { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 是否为http(s)://开头 + * + * @param link 链接 + * @return 结果 + */ + public static boolean ishttp(String link) + { + return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); + } + + /** + * 字符串转set + * + * @param str 字符串 + * @param sep 分隔符 + * @return set集合 + */ + public static final Set str2Set(String str, String sep) + { + return new HashSet(str2List(str, sep, true, false)); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @return list集合 + */ + public static final List str2List(String str, String sep) + { + return str2List(str, sep, true, false); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @param filterBlank 过滤纯空白 + * @param trim 去掉首尾空白 + * @return list集合 + */ + public static final List str2List(String str, String sep, boolean filterBlank, boolean trim) + { + List list = new ArrayList(); + if (StringUtils.isEmpty(str)) + { + return list; + } + + // 过滤空白字符串 + if (filterBlank && StringUtils.isBlank(str)) + { + return list; + } + String[] split = str.split(sep); + for (String string : split) + { + if (filterBlank && StringUtils.isBlank(string)) + { + continue; + } + if (trim) + { + string = string.trim(); + } + list.add(string); + } + + return list; + } + + /** + * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value + * + * @param collection 给定的集合 + * @param array 给定的数组 + * @return boolean 结果 + */ + public static boolean containsAny(Collection collection, String... array) + { + if (isEmpty(collection) || isEmpty(array)) + { + return false; + } + else + { + for (String str : array) + { + if (collection.contains(str)) + { + return true; + } + } + return false; + } + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + * + * @param cs 指定字符串 + * @param searchCharSequences 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + */ + public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) + { + if (isEmpty(cs) || isEmpty(searchCharSequences)) + { + return false; + } + for (CharSequence testStr : searchCharSequences) + { + if (containsIgnoreCase(cs, testStr)) + { + return true; + } + } + return false; + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) + { + if (str == null) + { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) + { + char c = str.charAt(i); + if (i > 0) + { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } + else + { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) + { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) + { + sb.append(SEPARATOR); + } + else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) + { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) + { + if (str != null && strs != null) + { + for (String s : strs) + { + if (str.equalsIgnoreCase(trim(s))) + { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) + { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) + { + // 没必要转换 + return ""; + } + else if (!name.contains("_")) + { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) + { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) + { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 + * 例如:user_name->userName + */ + public static String toCamelCase(String s) + { + if (s == null) + { + return null; + } + if (s.indexOf(SEPARATOR) == -1) + { + return s; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + + if (c == SEPARATOR) + { + upperCase = true; + } + else if (upperCase) + { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } + else + { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) + { + if (isEmpty(str) || isEmpty(strs)) + { + return false; + } + for (String pattern : strs) + { + if (isMatch(pattern, str)) + { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return + */ + public static boolean isMatch(String pattern, String url) + { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) + { + return (T) obj; + } + + /** + * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static final String padl(final Number num, final int size) + { + return padl(num.toString(), size, '0'); + } + + /** + * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static final String padl(final String s, final int size, final char c) + { + final StringBuilder sb = new StringBuilder(size); + if (s != null) + { + final int len = s.length(); + if (s.length() <= size) + { + for (int i = size - len; i > 0; i--) + { + sb.append(c); + } + sb.append(s); + } + else + { + return s.substring(len - size, len); + } + } + else + { + for (int i = size; i > 0; i--) + { + sb.append(c); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/Threads.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/Threads.java new file mode 100644 index 0000000..977eca8 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/Threads.java @@ -0,0 +1,96 @@ +package org.lingniu.idp.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.*; + +/** + * 线程相关工具类. + * + * @author portal + */ +public class Threads +{ + private static final Logger logger = LoggerFactory.getLogger(Threads.class); + + /** + * sleep等待,单位为毫秒 + */ + public static void sleep(long milliseconds) + { + try + { + Thread.sleep(milliseconds); + } + catch (InterruptedException e) + { + return; + } + } + + /** + * 停止线程池 + * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. + * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数. + * 如果仍然超時,則強制退出. + * 另对在shutdown时线程本身被调用中断做了处理. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool) + { + if (pool != null && !pool.isShutdown()) + { + pool.shutdown(); + try + { + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + pool.shutdownNow(); + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + logger.info("Pool did not terminate"); + } + } + } + catch (InterruptedException ie) + { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** + * 打印线程异常信息 + */ + public static void printException(Runnable r, Throwable t) + { + if (t == null && r instanceof Future) + { + try + { + Future future = (Future) r; + if (future.isDone()) + { + future.get(); + } + } + catch (CancellationException ce) + { + t = ce; + } + catch (ExecutionException ee) + { + t = ee.getCause(); + } + catch (InterruptedException ie) + { + Thread.currentThread().interrupt(); + } + } + if (t != null) + { + logger.error(t.getMessage(), t); + } + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/bean/BeanUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/bean/BeanUtils.java new file mode 100644 index 0000000..7f3625b --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/bean/BeanUtils.java @@ -0,0 +1,110 @@ +package org.lingniu.idp.utils.bean; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Bean 工具类 + * + * @author portal + */ +public class BeanUtils extends org.springframework.beans.BeanUtils +{ + /** Bean方法名中属性名开始的下标 */ + private static final int BEAN_METHOD_PROP_INDEX = 3; + + /** * 匹配getter方法的正则表达式 */ + private static final Pattern GET_PATTERN = Pattern.compile("get(\\p{javaUpperCase}\\w*)"); + + /** * 匹配setter方法的正则表达式 */ + private static final Pattern SET_PATTERN = Pattern.compile("set(\\p{javaUpperCase}\\w*)"); + + /** + * Bean属性复制工具方法。 + * + * @param dest 目标对象 + * @param src 源对象 + */ + public static void copyBeanProp(Object dest, Object src) + { + try + { + copyProperties(src, dest); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + /** + * 获取对象的setter方法。 + * + * @param obj 对象 + * @return 对象的setter方法列表 + */ + public static List getSetterMethods(Object obj) + { + // setter方法列表 + List setterMethods = new ArrayList(); + + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + + // 查找setter方法 + + for (Method method : methods) + { + Matcher m = SET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 1)) + { + setterMethods.add(method); + } + } + // 返回setter方法列表 + return setterMethods; + } + + /** + * 获取对象的getter方法。 + * + * @param obj 对象 + * @return 对象的getter方法列表 + */ + + public static List getGetterMethods(Object obj) + { + // getter方法列表 + List getterMethods = new ArrayList(); + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + // 查找getter方法 + for (Method method : methods) + { + Matcher m = GET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 0)) + { + getterMethods.add(method); + } + } + // 返回getter方法列表 + return getterMethods; + } + + /** + * 检查Bean方法名中的属性名是否相等。
+ * 如getName()和setName()属性名一样,getName()和setAge()属性名不一样。 + * + * @param m1 方法名1 + * @param m2 方法名2 + * @return 属性名一样返回true,否则返回false + */ + + public static boolean isMethodPropEquals(String m1, String m2) + { + return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX)); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/bean/BeanValidators.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/bean/BeanValidators.java new file mode 100644 index 0000000..f3e743d --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/bean/BeanValidators.java @@ -0,0 +1,26 @@ +package org.lingniu.idp.utils.bean; + + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; + +import java.util.Set; + +/** + * bean对象属性验证 + * + * @author portal + */ +public class BeanValidators +{ + public static void validateWithException(Validator validator, Object object, Class... groups) + throws ConstraintViolationException + { + Set> constraintViolations = validator.validate(object, groups); + if (!constraintViolations.isEmpty()) + { + throw new ConstraintViolationException(constraintViolations); + } + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/http/HttpHelper.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/http/HttpHelper.java new file mode 100644 index 0000000..8ab96b6 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/http/HttpHelper.java @@ -0,0 +1,56 @@ +package org.lingniu.idp.utils.http; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.ServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +/** + * 通用http工具封装 + * + * @author portal + */ +public class HttpHelper +{ + private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class); + + public static String getBodyString(ServletRequest request) + { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = null; + try (InputStream inputStream = request.getInputStream()) + { + reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = ""; + while ((line = reader.readLine()) != null) + { + sb.append(line); + } + } + catch (IOException e) + { + LOGGER.warn("getBodyString出现问题!"); + } + finally + { + if (reader != null) + { + try + { + reader.close(); + } + catch (IOException e) + { + LOGGER.error(ExceptionUtils.getMessage(e)); + } + } + } + return sb.toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/http/HttpUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/http/HttpUtils.java new file mode 100644 index 0000000..668c18a --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/http/HttpUtils.java @@ -0,0 +1,285 @@ +package org.lingniu.idp.utils.http; + +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.utils.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; + +import javax.net.ssl.*; +import java.io.*; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + +/** + * 通用http发送方法 + * + * @author portal + */ +public class HttpUtils +{ + private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url) + { + return sendGet(url, StringUtils.EMPTY); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param) + { + return sendGet(url, param, Constants.UTF8); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @param contentType 编码类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param, String contentType) + { + StringBuilder result = new StringBuilder(); + BufferedReader in = null; + try + { + String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url; + log.info("sendGet - {}", urlNameString); + URL realUrl = new URL(urlNameString); + URLConnection connection = realUrl.openConnection(); + connection.setRequestProperty("accept", "*/*"); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + connection.connect(); + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (in != null) + { + in.close(); + } + } + catch (Exception ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param) + { + return sendPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数 + * @param contentType 内容类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param, String contentType) + { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try + { + log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (out != null) + { + out.close(); + } + if (in != null) + { + in.close(); + } + } + catch (IOException ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + public static String sendSSLPost(String url, String param) + { + return sendSSLPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } + + public static String sendSSLPost(String url, String param, String contentType) + { + StringBuilder result = new StringBuilder(); + String urlNameString = url + "?" + param; + try + { + log.info("sendSSLPost - {}", urlNameString); + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom()); + URL console = new URL(urlNameString); + HttpsURLConnection conn = (HttpsURLConnection) console.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + + conn.setSSLSocketFactory(sc.getSocketFactory()); + conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); + conn.connect(); + InputStream is = conn.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String ret = ""; + while ((ret = br.readLine()) != null) + { + if (ret != null && !"".equals(ret.trim())) + { + result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); + } + } + log.info("recv - {}", result); + conn.disconnect(); + br.close(); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e); + } + return result.toString(); + } + + private static class TrustAnyTrustManager implements X509TrustManager + { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[] {}; + } + } + + private static class TrustAnyHostnameVerifier implements HostnameVerifier + { + @Override + public boolean verify(String hostname, SSLSession session) + { + return true; + } + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/http/UserAgentUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/http/UserAgentUtils.java new file mode 100644 index 0000000..e6e1b95 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/http/UserAgentUtils.java @@ -0,0 +1,255 @@ +package org.lingniu.idp.utils.http; + +import nl.basjes.parse.useragent.UserAgent; +import nl.basjes.parse.useragent.UserAgentAnalyzer; +import org.lingniu.idp.utils.StringUtils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * UserAgent解析工具类 + * + * @author portal + */ +public class UserAgentUtils +{ + public static final String UNKNOWN = ""; + + // 浏览器正则表达式模式 + private static final Pattern CHROME_PATTERN = Pattern.compile("Chrome/(\\d+)(?:\\.\\d+)*"); + private static final Pattern FIREFOX_PATTERN = Pattern.compile("Firefox/(\\d+)(?:\\.\\d+)*"); + private static final Pattern EDGE_PATTERN = Pattern.compile("Edg(?:e)?/(\\d+)(?:\\.\\d+)*"); + private static final Pattern SAFARI_PATTERN = Pattern.compile("Version/(\\d+)(?:\\.\\d+)*"); + private static final Pattern OPERA_PATTERN = Pattern.compile("Opera/(\\d+)(?:\\.\\d+)*"); + private static final Pattern IE_PATTERN = Pattern.compile("(?:MSIE |Trident/.*rv:)(\\d+)(?:\\.\\d+)*"); + private static final Pattern SAMSUNG_PATTERN = Pattern.compile("SamsungBrowser/(\\d+)(?:\\.\\d+)*"); + private static final Pattern UC_PATTERN = Pattern.compile("UCBrowser/(\\d+)(?:\\.\\d+)*"); + private static final Pattern QQ_PATTERN = Pattern.compile("QQBrowser/(\\d+)(?:\\.\\d+)*"); + private static final Pattern WECHAT_PATTERN = Pattern.compile("MicroMessenger/(\\d+)(?:\\.\\d+)*"); + private static final Pattern BAIDU_PATTERN = Pattern.compile("baidubrowser/(\\d+)(?:\\.\\d+)*"); + + // 操作系统正则表达式模式 + private static final Pattern WINDOWS_PATTERN = Pattern.compile("Windows NT (\\d+\\.\\d+)"); + private static final Pattern MACOS_PATTERN = Pattern.compile("Mac OS X (\\d+[_\\d]*)"); + private static final Pattern ANDROID_PATTERN = Pattern.compile("Android (\\d+)(?:\\.\\d+)*"); + private static final Pattern IOS_PATTERN = Pattern.compile("OS[\\s_](\\d+)(?:_\\d+)*"); + private static final Pattern LINUX_PATTERN = Pattern.compile("Linux"); + private static final Pattern CHROMEOS_PATTERN = Pattern.compile("CrOS"); + + private static final UserAgentAnalyzer userAgentAnalyzer = UserAgentAnalyzer + .newBuilder().hideMatcherLoadStats() + .withCache(5000) + .showMinimalVersion() + .withField(UserAgent.AGENT_NAME_VERSION) + .withField(UserAgent.OPERATING_SYSTEM_NAME_VERSION) + .build(); + + /** + * 获取客户端浏览器 + */ + public static String getBrowser(String userAgent) + { + UserAgent.ImmutableUserAgent iua = userAgentAnalyzer.parse(userAgent); + String agentNameVersion = iua.get(UserAgent.AGENT_NAME_VERSION).getValue(); + if (StringUtils.isBlank(agentNameVersion) || agentNameVersion.contains("??")) + { + return formatBrowser(userAgent); + } + return agentNameVersion; + } + + /** + * 获取客户端操作系统 + */ + public static String getOperatingSystem(String userAgent) + { + UserAgent.ImmutableUserAgent iua = userAgentAnalyzer.parse(userAgent); + String operatingSystemNameVersion = iua.get(UserAgent.OPERATING_SYSTEM_NAME_VERSION).getValue(); + if (StringUtils.isBlank(operatingSystemNameVersion) || operatingSystemNameVersion.contains("??")) + { + return formatOperatingSystem(userAgent); + } + return operatingSystemNameVersion; + } + + /** + * 全面浏览器检测 + */ + private static String formatBrowser(String browser) + { + // Chrome系列浏览器 + Matcher chromeMatcher = CHROME_PATTERN.matcher(browser); + if (chromeMatcher.find() && (browser.contains("Chrome") || browser.contains("CriOS"))) + { + return "Chrome" + chromeMatcher.group(1); + } + // Firefox + Matcher firefoxMatcher = FIREFOX_PATTERN.matcher(browser); + if (firefoxMatcher.find()) + { + return "Firefox" + firefoxMatcher.group(1); + } + // Edge浏览器 + Matcher edgeMatcher = EDGE_PATTERN.matcher(browser); + if (edgeMatcher.find()) + { + return "Edge" + edgeMatcher.group(1); + } + // Safari浏览器(需排除Chrome) + Matcher safariMatcher = SAFARI_PATTERN.matcher(browser); + if (safariMatcher.find() && !browser.contains("Chrome")) + { + return "Safari" + safariMatcher.group(1); + } + // 微信内置浏览器 + Matcher wechatMatcher = WECHAT_PATTERN.matcher(browser); + if (wechatMatcher.find()) + { + return "WeChat" + wechatMatcher.group(1); + } + // UC浏览器 + Matcher ucMatcher = UC_PATTERN.matcher(browser); + if (ucMatcher.find()) + { + return "UC Browser" + ucMatcher.group(1); + } + // QQ浏览器 + Matcher qqMatcher = QQ_PATTERN.matcher(browser); + if (qqMatcher.find()) + { + return "QQ Browser" + qqMatcher.group(1); + } + // 百度浏览器 + Matcher baiduMatcher = BAIDU_PATTERN.matcher(browser); + if (baiduMatcher.find()) + { + return "Baidu Browser" + baiduMatcher.group(1); + } + // Samsung浏览器 + Matcher samsungMatcher = SAMSUNG_PATTERN.matcher(browser); + if (samsungMatcher.find()) + { + return "Samsung Browser" + samsungMatcher.group(1); + } + // Opera浏览器 + Matcher operaMatcher = OPERA_PATTERN.matcher(browser); + if (operaMatcher.find()) + { + return "Opera" + operaMatcher.group(1); + } + // IE浏览器 + Matcher ieMatcher = IE_PATTERN.matcher(browser); + if (ieMatcher.find()) + { + return "Internet Explorer" + ieMatcher.group(1); + } + return UNKNOWN; + } + + /** + * 检测操作系统 + */ + private static String formatOperatingSystem(String operatingSystem) + { + // Windows系统 + Matcher windowsMatcher = WINDOWS_PATTERN.matcher(operatingSystem); + if (windowsMatcher.find()) + { + return "Windows" + getWindowsVersionDisplay(windowsMatcher.group(1)); + } + // macOS系统 + Matcher macMatcher = MACOS_PATTERN.matcher(operatingSystem); + if (macMatcher.find()) + { + String version = macMatcher.group(1).replace("_", "."); + return "macOS" + extractMajorVersion(version); + } + // Android系统 + Matcher androidMatcher = ANDROID_PATTERN.matcher(operatingSystem); + if (androidMatcher.find()) + { + return "Android" + extractMajorVersion(androidMatcher.group(1)); + } + // iOS系统 + Matcher iosMatcher = IOS_PATTERN.matcher(operatingSystem); + if (iosMatcher.find() && (operatingSystem.contains("iPhone") || operatingSystem.contains("iPad"))) + { + return "iOS" + extractMajorVersion(iosMatcher.group(1)); + } + // Linux系统 + if (LINUX_PATTERN.matcher(operatingSystem).find() && !operatingSystem.contains("Android")) + { + return "Linux"; + } + // Chrome OS + if (CHROMEOS_PATTERN.matcher(operatingSystem).find()) + { + return "Chrome OS"; + } + return UNKNOWN; + } + + /** + * 提取优化的主版本号 + */ + private static String extractMajorVersion(String fullVersion) + { + if (StringUtils.isEmpty(fullVersion)) + { + return StringUtils.EMPTY; + } + try + { + // 清理版本号中的非数字字符 + String cleanVersion = fullVersion.replaceAll("[^0-9.]", ""); + String[] parts = cleanVersion.split("\\."); + if (parts.length > 0) + { + String firstPart = parts[0]; + if (firstPart.matches("\\d+")) + { + int version = Integer.parseInt(firstPart); + + // 处理三位数版本号(如142 -> 14) + if (version >= 100) + { + return String.valueOf(version / 10); + } + return firstPart; + } + } + } + catch (NumberFormatException e) + { + // 版本号解析失败,返回原始值 + } + return fullVersion; + } + + /** + * Windows版本号显示优化 + */ + private static String getWindowsVersionDisplay(String version) + { + switch (version) + { + case "10.0": + return "10"; + case "6.3": + return "8.1"; + case "6.2": + return "8"; + case "6.1": + return "7"; + case "6.0": + return "Vista"; + case "5.1": + return "XP"; + case "5.0": + return "2000"; + default: + return extractMajorVersion(version); + } + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ip/AddressUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ip/AddressUtils.java new file mode 100644 index 0000000..4c065fb --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ip/AddressUtils.java @@ -0,0 +1,57 @@ +package org.lingniu.idp.utils.ip; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import org.lingniu.idp.config.ProjectConfig; +import org.lingniu.idp.constant.Constants; +import org.lingniu.idp.utils.StringUtils; +import org.lingniu.idp.utils.http.HttpUtils; +import org.lingniu.idp.utils.ip.IpUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 获取地址类 + * + * @author portal + */ +public class AddressUtils +{ + private static final Logger log = LoggerFactory.getLogger(AddressUtils.class); + + // IP地址查询 + public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp"; + + // 未知地址 + public static final String UNKNOWN = "XX XX"; + + public static String getRealAddressByIP(String ip) + { + // 内网不查询 + if (IpUtils.internalIp(ip)) + { + return "内网IP"; + } + if (ProjectConfig.isAddressEnabled()) + { + try + { + String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", Constants.GBK); + if (StringUtils.isEmpty(rspStr)) + { + log.error("获取地理位置异常 {}", ip); + return UNKNOWN; + } + JSONObject obj = JSON.parseObject(rspStr); + String region = obj.getString("pro"); + String city = obj.getString("city"); + return String.format("%s %s", region, city); + } + catch (Exception e) + { + log.error("获取地理位置异常 {}", ip); + } + } + return UNKNOWN; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ip/IpUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ip/IpUtils.java new file mode 100644 index 0000000..7a32d77 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/ip/IpUtils.java @@ -0,0 +1,383 @@ +package org.lingniu.idp.utils.ip; + +import org.lingniu.idp.utils.ServletUtils; +import org.lingniu.idp.utils.StringUtils; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 获取IP方法 + * + * @author portal + */ +public class IpUtils +{ + public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)"; + // 匹配 ip + public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")"; + public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))"; + // 匹配网段 + public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")"; + + /** + * 获取客户端IP + * + * @return IP地址 + */ + public static String getIpAddr() + { + return getIpAddr(ServletUtils.getRequest()); + } + + /** + * 获取客户端IP + * + * @param request 请求对象 + * @return IP地址 + */ + public static String getIpAddr(HttpServletRequest request) + { + if (request == null) + { + return "unknown"; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Forwarded-For"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) + { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) + { + if (StringUtils.isNull(addr) || addr.length < 2) + { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) + { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) + { + return true; + } + case SECTION_5: + switch (b1) + { + case SECTION_6: + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) + { + if (text.length() == 0) + { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try + { + long l; + int i; + switch (elements.length) + { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) + { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) + { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) + { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } + catch (NumberFormatException e) + { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() + { + try + { + return InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException e) + { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException e) + { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) + { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) + { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) + { + if (false == isUnknown(subIp)) + { + ip = subIp; + break; + } + } + } + return StringUtils.substring(ip, 0, 255); + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) + { + return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + + /** + * 是否为IP + */ + public static boolean isIP(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP); + } + + /** + * 是否为IP,或 *为间隔的通配符地址 + */ + public static boolean isIpWildCard(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD); + } + + /** + * 检测参数是否在ip通配符里 + */ + public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip) + { + String[] s1 = ipWildCard.split("\\."); + String[] s2 = ip.split("\\."); + boolean isMatchedSeg = true; + for (int i = 0; i < s1.length && !s1[i].equals("*"); i++) + { + if (!s1[i].equals(s2[i])) + { + isMatchedSeg = false; + break; + } + } + return isMatchedSeg; + } + + /** + * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串 + */ + public static boolean isIPSegment(String ipSeg) + { + return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG); + } + + /** + * 判断ip是否在指定网段中 + */ + public static boolean ipIsInNetNoCheck(String iparea, String ip) + { + int idx = iparea.indexOf('-'); + String[] sips = iparea.substring(0, idx).split("\\."); + String[] sipe = iparea.substring(idx + 1).split("\\."); + String[] sipt = ip.split("\\."); + long ips = 0L, ipe = 0L, ipt = 0L; + for (int i = 0; i < 4; ++i) + { + ips = ips << 8 | Integer.parseInt(sips[i]); + ipe = ipe << 8 | Integer.parseInt(sipe[i]); + ipt = ipt << 8 | Integer.parseInt(sipt[i]); + } + if (ips > ipe) + { + long t = ips; + ips = ipe; + ipe = t; + } + return ips <= ipt && ipt <= ipe; + } + + /** + * 校验ip是否符合过滤串规则 + * + * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99` + * @param ip 校验IP地址 + * @return boolean 结果 + */ + public static boolean isMatchedIp(String filter, String ip) + { + if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip)) + { + return false; + } + String[] ips = filter.split(";"); + for (String iStr : ips) + { + if (isIP(iStr) && iStr.equals(ip)) + { + return true; + } + else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip)) + { + return true; + } + else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip)) + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/jwt/Jwks.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/jwt/Jwks.java new file mode 100644 index 0000000..cc82f34 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/jwt/Jwks.java @@ -0,0 +1,83 @@ +package org.lingniu.idp.utils.jwt; + +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.KeyUse; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Date; +import java.util.UUID; + +public class Jwks { + + /** + * 从现有的RSA密钥生成JWK + */ + public static RSAKey generateRsa(RSAPublicKey publicKey, RSAPrivateKey privateKey, String keyId) { + RSAKey.Builder builder = new RSAKey.Builder(publicKey) + .keyID(keyId) + .keyUse(KeyUse.SIGNATURE) + .algorithm(JWSAlgorithm.RS256); + + if (privateKey != null) { + builder.privateKey(privateKey); + } + + return builder.build(); + } + + /** + * 仅生成公钥JWK(用于验证端) + */ + public static RSAKey generatePublicJwk(RSAPublicKey publicKey, String keyId) { + return new RSAKey.Builder(publicKey) + .keyID(keyId) + .keyUse(KeyUse.SIGNATURE) + .algorithm(JWSAlgorithm.RS256) + .build(); + } + + /** + * 生成新的RSA密钥对(仅用于开发或测试) + */ + public static RSAKey generateRsa() { + return generateRsa(UUID.randomUUID().toString()); + } + + public static RSAKey generateRsa(String keyId) { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(keyId) + .keyUse(KeyUse.SIGNATURE) + .algorithm(JWSAlgorithm.RS256) + .build(); + } catch (Exception e) { + throw new RuntimeException("生成RSA密钥失败", e); + } + } + + /** + * 创建带时间戳的密钥ID + */ + public static String generateKeyIdWithTimestamp(String prefix) { + return prefix + "-" + System.currentTimeMillis(); + } + + /** + * 获取默认密钥ID + */ + public static String getDefaultKeyId() { + return "jwt-key-" + new Date().getTime(); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/jwt/JwtUtil.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/jwt/JwtUtil.java new file mode 100644 index 0000000..2da36c6 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/jwt/JwtUtil.java @@ -0,0 +1,389 @@ +package org.lingniu.idp.utils.jwt; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import lombok.Getter; +import org.lingniu.idp.config.JwtProperties; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.text.ParseException; +import java.util.Base64; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +@Component +public class JwtUtil { + + @Getter + private final RSAPrivateKey privateKey; + @Getter + private final RSAPublicKey publicKey; + private final JWSHeader header; + private final long expiration; + private final String tokenPrefix; + + // 构造函数 + public JwtUtil(JwtProperties jwtProperties) throws Exception { + this.expiration = jwtProperties.getExpiration().toMillis(); + this.tokenPrefix = jwtProperties.getPrefix(); + + JwtProperties.RsaKey rsaConfig = jwtProperties.getRsa(); + + // 加载或生成密钥 + if (StringUtils.hasText(rsaConfig.getPrivateKey()) && + StringUtils.hasText(rsaConfig.getPublicKey())) { + // 从配置加载密钥 + this.privateKey = loadPrivateKey(rsaConfig.getPrivateKey()); + this.publicKey = loadPublicKey(rsaConfig.getPublicKey()); + } else { + // 自动生成密钥(仅用于开发) + KeyPair keyPair = generateKeyPair(rsaConfig.getKeySize()); + this.privateKey = (RSAPrivateKey) keyPair.getPrivate(); + this.publicKey = (RSAPublicKey) keyPair.getPublic(); + + System.out.println("⚠️ 未配置RSA密钥,已自动生成新的密钥对"); + System.out.println("公钥(保存到配置中):"); + System.out.println(toPemFormat(publicKey)); + System.out.println("\n私钥(保存到配置中):"); + System.out.println(toPemFormat(privateKey)); + } + + // 创建JWT头部 + this.header = new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID("jwt-key-" + System.currentTimeMillis()) + .build(); + } + + /** + * 加载私钥 + */ + private RSAPrivateKey loadPrivateKey(String keyContent) throws Exception { + // 检查是否是文件路径 + if (keyContent.startsWith("classpath:") || keyContent.startsWith("file:")) { + keyContent = loadKeyFromFile(keyContent); + } + + // 清理PEM格式 + String cleanedKey = cleanPemKey(keyContent, "PRIVATE"); + + // Base64解码 + byte[] keyBytes = Base64.getDecoder().decode(cleanedKey); + + // 创建密钥规范 + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + + return (RSAPrivateKey) keyFactory.generatePrivate(spec); + } + + /** + * 加载公钥 + */ + private RSAPublicKey loadPublicKey(String keyContent) throws Exception { + // 检查是否是文件路径 + if (keyContent.startsWith("classpath:") || keyContent.startsWith("file:")) { + keyContent = loadKeyFromFile(keyContent); + } + + // 清理PEM格式 + String cleanedKey = cleanPemKey(keyContent, "PUBLIC"); + + // Base64解码 + byte[] keyBytes = Base64.getDecoder().decode(cleanedKey); + + // 创建密钥规范 + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + + return (RSAPublicKey) keyFactory.generatePublic(spec); + } + + /** + * 从文件加载密钥 + */ + private String loadKeyFromFile(String filePath) throws Exception { + Resource resource; + if (filePath.startsWith("classpath:")) { + resource = new ClassPathResource(filePath.substring(10)); + } else if (filePath.startsWith("file:")) { + resource = new FileSystemResource(filePath.substring(5)); + } else { + resource = new ClassPathResource(filePath); + } + + if (!resource.exists()) { + throw new IllegalArgumentException("密钥文件不存在: " + filePath); + } + + byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream()); + return new String(bytes); + } + + /** + * 清理PEM密钥 + */ + private String cleanPemKey(String key, String keyType) { + if (!StringUtils.hasText(key)) { + throw new IllegalArgumentException(keyType + "密钥内容为空"); + } + + // 移除所有空白字符 + key = key.replaceAll("\\s+", ""); + + // 如果没有PEM标记,直接返回 + if (!key.contains("-----BEGIN")) { + return key; + } + + // 提取PEM内容 + String beginMarker = "-----BEGIN" + keyType + "KEY-----"; + String endMarker = "-----END" + keyType + "KEY-----"; + + int beginIndex = key.indexOf(beginMarker); + if (beginIndex == -1) { + beginMarker = "-----BEGIN" + keyType + " KEY-----"; + beginIndex = key.indexOf(beginMarker); + } + + if (beginIndex == -1) { + throw new IllegalArgumentException("无效的PEM格式: 找不到BEGIN标记"); + } + + beginIndex += beginMarker.length(); + int endIndex = key.indexOf(endMarker, beginIndex); + if (endIndex == -1) { + endMarker = "-----END" + keyType + " KEY-----"; + endIndex = key.indexOf(endMarker, beginIndex); + } + + if (endIndex == -1) { + throw new IllegalArgumentException("无效的PEM格式: 找不到END标记"); + } + + return key.substring(beginIndex, endIndex).replaceAll("\\s+", ""); + } + + /** + * 生成密钥对 + */ + private KeyPair generateKeyPair(int keySize) throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(keySize); + return keyGen.generateKeyPair(); + } + + /** + * 创建JWT令牌 + */ + public String createToken(Map claims) { + return createToken(claims, this.expiration); + } + + public String createToken(Map claims, long expirationMs) { + try { + // 构建声明 + JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); + + // 添加自定义声明 + for (Map.Entry entry : claims.entrySet()) { + claimsBuilder.claim(entry.getKey(), entry.getValue()); + } + + Date now = new Date(); + claimsBuilder + .issueTime(now) + .expirationTime(new Date(now.getTime() + expirationMs)) + .notBeforeTime(now) + .jwtID(UUID.randomUUID().toString()); // 唯一的JWT ID + + JWTClaimsSet claimsSet = claimsBuilder.build(); + + // 创建签名JWT + SignedJWT signedJWT = new SignedJWT(header, claimsSet); + + // 使用RSA私钥进行签名 + JWSSigner signer = new RSASSASigner(this.privateKey); + signedJWT.sign(signer); + + return signedJWT.serialize(); + + } catch (JOSEException e) { + throw new JwtException("创建JWT令牌失败", e); + } + } + + /** + * 解析并验证JWT令牌 + */ + public JWTClaimsSet parseToken(String token) { + try { + // 提取实际令牌(去除Bearer前缀) + String actualToken = extractToken(token); + + // 解析JWT + SignedJWT signedJWT = SignedJWT.parse(actualToken); + + // 使用RSA公钥验证签名 + JWSVerifier verifier = new RSASSAVerifier(this.publicKey); + if (!signedJWT.verify(verifier)) { + throw new JwtException("JWT签名验证失败"); + } + + // 获取声明集 + JWTClaimsSet claims = signedJWT.getJWTClaimsSet(); + + // 验证过期时间 + if (claims.getExpirationTime() != null && + claims.getExpirationTime().before(new Date())) { + throw new JwtException("JWT令牌已过期"); + } + + // 验证生效时间 + if (claims.getNotBeforeTime() != null && + claims.getNotBeforeTime().after(new Date())) { + throw new JwtException("JWT令牌尚未生效"); + } + + return claims; + + } catch (ParseException e) { + throw new JwtException("JWT令牌解析失败", e); + } catch (JOSEException e) { + throw new JwtException("JWT签名验证异常", e); + } + } + + /** + * 提取令牌(去除前缀) + */ + public String extractToken(String authHeader) { + if (authHeader != null && authHeader.startsWith(tokenPrefix)) { + return authHeader.substring(tokenPrefix.length()).trim(); + } + return authHeader; + } + + /** + * 获取Base64编码的公钥 + */ + public String getPublicKeyBase64() { + return Base64.getEncoder().encodeToString(this.publicKey.getEncoded()); + } + + /** + * 获取PEM格式的公钥 + */ + public String toPemFormat(RSAPublicKey publicKey) { + String base64Key = Base64.getEncoder().encodeToString(publicKey.getEncoded()); + return formatAsPem(base64Key, "PUBLIC"); + } + + /** + * 获取PEM格式的私钥 + */ + public String toPemFormat(RSAPrivateKey privateKey) { + String base64Key = Base64.getEncoder().encodeToString(privateKey.getEncoded()); + return formatAsPem(base64Key, "PRIVATE"); + } + + /** + * 格式化PEM + */ + private String formatAsPem(String base64Key, String keyType) { + StringBuilder pem = new StringBuilder(); + pem.append("-----BEGIN ").append(keyType).append(" KEY-----\n"); + + // 每64个字符换行 + for (int i = 0; i < base64Key.length(); i += 64) { + pem.append(base64Key, i, Math.min(base64Key.length(), i + 64)).append("\n"); + } + + pem.append("-----END ").append(keyType).append(" KEY-----\n"); + return pem.toString(); + } + + /** + * 验证令牌是否有效 + */ + public boolean validateToken(String token) { + try { + parseToken(token); + return true; + } catch (JwtException e) { + return false; + } + } + + /** + * 获取令牌中的声明值 + */ + public Object getClaim(String token, String claimName) { + JWTClaimsSet claims = parseToken(token); + return claims.getClaim(claimName); + } + + /** + * 获取令牌中的用户名(假设存储在"username"声明中) + */ + public String getUsername(String token) { + try { + return parseToken(token).getStringClaim("username"); + } catch (ParseException e) { + throw new JwtException("获取用户名失败", e); + } + } + + /** + * 获取令牌过期时间 + */ + public Date getExpiration(String token) { + return parseToken(token).getExpirationTime(); + } + + /** + * 获取令牌签发时间 + */ + public Date getIssuedAt(String token) { + return parseToken(token).getIssueTime(); + } + + /** + * 获取JWT头部信息 + */ + public JWSHeader getHeader(String token) { + try { + return SignedJWT.parse(extractToken(token)).getHeader(); + } catch (ParseException e) { + throw new JwtException("获取JWT头部失败", e); + } + } + + /** + * 自定义异常类 + */ + public static class JwtException extends RuntimeException { + public JwtException(String message) { + super(message); + } + public JwtException(String message, Throwable cause) { + super(message, cause); + } + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/jwt/RsaKeyGenerator.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/jwt/RsaKeyGenerator.java new file mode 100644 index 0000000..bc7fa24 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/jwt/RsaKeyGenerator.java @@ -0,0 +1,31 @@ +package org.lingniu.idp.utils.jwt; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Base64; + +public class RsaKeyGenerator { + public static void main(String[] args) throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); // 密钥长度 + KeyPair keyPair = keyGen.generateKeyPair(); + + PrivateKey privateKey = keyPair.getPrivate(); + PublicKey publicKey = keyPair.getPublic(); + + // 输出Base64编码的密钥 + System.out.println("=== 私钥(用于签名) ==="); + System.out.println(Base64.getEncoder().encodeToString(privateKey.getEncoded())); + + System.out.println("\n=== 公钥(用于验证) ==="); + System.out.println(Base64.getEncoder().encodeToString(publicKey.getEncoded())); + + // 也可以输出PEM格式 + System.out.println("\n=== PEM格式私钥 ==="); + System.out.println("-----BEGIN PRIVATE KEY-----"); + System.out.println(Base64.getEncoder().encodeToString(privateKey.getEncoded())); + System.out.println("-----END PRIVATE KEY-----"); + } +} \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/sign/Base64.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/sign/Base64.java new file mode 100644 index 0000000..911378f --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/sign/Base64.java @@ -0,0 +1,291 @@ +package org.lingniu.idp.utils.sign; + +/** + * Base64工具类 + * + * @author portal + */ +public final class Base64 +{ + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static final private byte[] base64Alphabet = new byte[BASELENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static + { + for (int i = 0; i < BASELENGTH; ++i) + { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) + { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) + { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) + { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) + { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + } + + private static boolean isWhiteSpace(char octect) + { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) + { + return (octect == PAD); + } + + private static boolean isData(char octect) + { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) + { + if (binaryData == null) + { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) + { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) + { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) + { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } + else if (fewerThan24bits == SIXTEENBIT) + { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) + { + if (encoded == null) + { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) + { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) + { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) + { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) + { + return null; + } // if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) + { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) + {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) + { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } + else if (!isPad(d3) && isPad(d4)) + { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } + else + { + return null; + } + } + else + { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) + { + if (data == null) + { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) + { + if (!isWhiteSpace(data[i])) + { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/sign/Md5Utils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/sign/Md5Utils.java new file mode 100644 index 0000000..430c9e7 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/sign/Md5Utils.java @@ -0,0 +1,68 @@ +package org.lingniu.idp.utils.sign; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +/** + * Md5加密方法 + * + * @author portal + */ +public class Md5Utils +{ + private static final Logger log = LoggerFactory.getLogger(Md5Utils.class); + + private static byte[] md5(String s) + { + MessageDigest algorithm; + try + { + algorithm = MessageDigest.getInstance("MD5"); + algorithm.reset(); + algorithm.update(s.getBytes("UTF-8")); + byte[] messageDigest = algorithm.digest(); + return messageDigest; + } + catch (Exception e) + { + log.error("MD5 Error...", e); + } + return null; + } + + private static final String toHex(byte hash[]) + { + if (hash == null) + { + return null; + } + StringBuffer buf = new StringBuffer(hash.length * 2); + int i; + + for (i = 0; i < hash.length; i++) + { + if ((hash[i] & 0xff) < 0x10) + { + buf.append("0"); + } + buf.append(Long.toString(hash[i] & 0xff, 16)); + } + return buf.toString(); + } + + public static String hash(String s) + { + try + { + return new String(toHex(md5(s)).getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + } + catch (Exception e) + { + log.error("not supported charset...{}", e); + return s; + } + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/spring/SpringUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/spring/SpringUtils.java new file mode 100644 index 0000000..48b871a --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/spring/SpringUtils.java @@ -0,0 +1,164 @@ +package org.lingniu.idp.utils.spring; + +import org.lingniu.idp.utils.StringUtils; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * spring工具类 方便在非spring管理环境中获取bean + * + * @author portal + */ +@Component +public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware +{ + /** Spring应用上下文环境 */ + private static ConfigurableListableBeanFactory beanFactory; + + private static ApplicationContext applicationContext; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException + { + SpringUtils.beanFactory = beanFactory; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + SpringUtils.applicationContext = applicationContext; + } + + /** + * 获取对象 + * + * @param name + * @return Object 一个以所给名字注册的bean的实例 + * @throws BeansException + * + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) throws BeansException + { + return (T) beanFactory.getBean(name); + } + + /** + * 获取类型为requiredType的对象 + * + * @param clz + * @return + * @throws BeansException + * + */ + public static T getBean(Class clz) throws BeansException + { + T result = (T) beanFactory.getBean(clz); + return result; + } + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + * + * @param name + * @return boolean + */ + public static boolean containsBean(String name) + { + return beanFactory.containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + * @param name + * @return boolean + * @throws NoSuchBeanDefinitionException + * + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.isSingleton(name); + } + + /** + * @param name + * @return Class 注册对象的类型 + * @throws NoSuchBeanDefinitionException + * + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + * + * @param name + * @return + * @throws NoSuchBeanDefinitionException + * + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getAliases(name); + } + + /** + * 获取aop代理对象 + * + * @param invoker + * @return + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) + { + Object proxy = AopContext.currentProxy(); + if (((Advised) proxy).getTargetSource().getTargetClass() == invoker.getClass()) + { + return (T) proxy; + } + return invoker; + } + + /** + * 获取当前的环境配置,无配置返回null + * + * @return 当前的环境配置 + */ + public static String[] getActiveProfiles() + { + return applicationContext.getEnvironment().getActiveProfiles(); + } + + /** + * 获取当前的环境配置,当有多个环境配置时,只获取第一个 + * + * @return 当前的环境配置 + */ + public static String getActiveProfile() + { + final String[] activeProfiles = getActiveProfiles(); + return StringUtils.isNotEmpty(activeProfiles) ? activeProfiles[0] : null; + } + + /** + * 获取配置文件中的值 + * + * @param key 配置文件的key + * @return 当前的配置文件的值 + * + */ + public static String getRequiredProperty(String key) + { + return applicationContext.getEnvironment().getRequiredProperty(key); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/text/CharsetKit.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/text/CharsetKit.java new file mode 100644 index 0000000..a1281e6 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/text/CharsetKit.java @@ -0,0 +1,87 @@ +package org.lingniu.idp.utils.text; + +import org.lingniu.idp.utils.StringUtils; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * 字符集工具类 + * + * @author portal + */ +public class CharsetKit +{ + /** ISO-8859-1 */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** UTF-8 */ + public static final String UTF_8 = "UTF-8"; + /** GBK */ + public static final String GBK = "GBK"; + + /** ISO-8859-1 */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** UTF-8 */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** GBK */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) + { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) + { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) + { + if (null == srcCharset) + { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) + { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) + { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * @return 系统字符集编码 + */ + public static String systemCharset() + { + return Charset.defaultCharset().name(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/text/Convert.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/text/Convert.java new file mode 100644 index 0000000..fc710be --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/text/Convert.java @@ -0,0 +1,1020 @@ +package org.lingniu.idp.utils.text; + +import org.lingniu.idp.utils.text.CharsetKit; +import org.lingniu.idp.utils.StringUtils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Set; + +/** + * 类型转换器 + * + * @author portal + */ +public class Convert +{ + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof String) + { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) + { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof Character) + { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) + { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Byte) + { + return (Byte) value; + } + if (value instanceof Number) + { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Byte.parseByte(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) + { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Short) + { + return (Short) value; + } + if (value instanceof Number) + { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Short.parseShort(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) + { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Number) + { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return NumberFormat.getInstance().parse(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) + { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Integer) + { + return (Integer) value; + } + if (value instanceof Number) + { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Integer.parseInt(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) + { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) + { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) + { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Integer[] {}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Long[] {}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) + { + if (StringUtils.isEmpty(str)) + { + return new String[] {}; + } + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) + { + return str.split(split); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Long) + { + return (Long) value; + } + if (value instanceof Number) + { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) + { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Double) + { + return (Double) value; + } + if (value instanceof Number) + { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) + { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Float) + { + return (Float) value; + } + if (value instanceof Number) + { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Float.parseFloat(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) + { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no、1、0、是、否, 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Boolean) + { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) + { + case "true": + case "yes": + case "ok": + case "1": + case "是": + return true; + case "false": + case "no": + case "0": + case "否": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) + { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) + { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Enum.valueOf(clazz, valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) + { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigInteger) + { + return (BigInteger) value; + } + if (value instanceof Long) + { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigInteger(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) + { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigDecimal) + { + return (BigDecimal) value; + } + if (value instanceof Long) + { + return new BigDecimal((Long) value); + } + if (value instanceof Double) + { + return BigDecimal.valueOf((Double) value); + } + if (value instanceof Integer) + { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigDecimal(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) + { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) + { + return str(obj, CharsetKit.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) + { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) + { + if (null == obj) + { + return null; + } + + if (obj instanceof String) + { + return (String) obj; + } + else if (obj instanceof byte[] || obj instanceof Byte[]) + { + if (obj instanceof byte[]) + { + return str((byte[]) obj, charset); + } + else + { + Byte[] bytes = (Byte[]) obj; + int length = bytes.length; + byte[] dest = new byte[length]; + for (int i = 0; i < length; i++) + { + dest[i] = bytes[i]; + } + return str(dest, charset); + } + } + else if (obj instanceof ByteBuffer) + { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) + { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) + { + if (data == null) + { + return null; + } + + if (null == charset) + { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) + { + if (data == null) + { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) + { + if (null == charset) + { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) + { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) + { + char[] c = input.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') + { + c[i] = '\u3000'; + } + else if (c[i] < '\177') + { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) + { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) + { + char[] c = text.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') + { + c[i] = ' '; + } + else if (c[i] > '\uFF00' && c[i] < '\uFF5F') + { + c[i] = (char) (c[i] - 65248); + } + } + return new String(c); + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) + { + String[] fraction = { "角", "分" }; + String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; + String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } }; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) + { + // 优化double计算精度丢失问题 + BigDecimal nNum = new BigDecimal(n); + BigDecimal decimal = new BigDecimal(10); + BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN); + double d = scale.doubleValue(); + s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) + { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) + { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) + { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/text/StrFormatter.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/text/StrFormatter.java new file mode 100644 index 0000000..817c7b2 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/text/StrFormatter.java @@ -0,0 +1,92 @@ +package org.lingniu.idp.utils.text; + +import org.lingniu.idp.utils.StringUtils; + +/** + * 字符串格式化 + * + * @author portal + */ +public class StrFormatter +{ + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) + { + if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) + { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) + { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) + { + if (handledPosition == 0) + { + return strPattern; + } + else + { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } + else + { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) + { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) + { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + else + { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } + else + { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/uuid/IdUtils.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/uuid/IdUtils.java new file mode 100644 index 0000000..90af149 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/uuid/IdUtils.java @@ -0,0 +1,49 @@ +package org.lingniu.idp.utils.uuid; + +/** + * ID生成器工具类 + * + * @author portal + */ +public class IdUtils +{ + /** + * 获取随机UUID + * + * @return 随机UUID + */ + public static String randomUUID() + { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + */ + public static String simpleUUID() + { + return UUID.randomUUID().toString(true); + } + + /** + * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 随机UUID + */ + public static String fastUUID() + { + return UUID.fastUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 简化的UUID,去掉了横线 + */ + public static String fastSimpleUUID() + { + return UUID.fastUUID().toString(true); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/uuid/Seq.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/uuid/Seq.java new file mode 100644 index 0000000..21ff833 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/uuid/Seq.java @@ -0,0 +1,87 @@ +package org.lingniu.idp.utils.uuid; + +import org.lingniu.idp.utils.DateUtils; +import org.lingniu.idp.utils.StringUtils; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author portal 序列生成类 + */ +public class Seq +{ + // 通用序列类型 + public static final String commSeqType = "COMMON"; + + // 上传序列类型 + public static final String uploadSeqType = "UPLOAD"; + + // 通用接口序列数 + private static AtomicInteger commSeq = new AtomicInteger(1); + + // 上传接口序列数 + private static AtomicInteger uploadSeq = new AtomicInteger(1); + + // 机器标识 + private static final String machineCode = "A"; + + /** + * 获取通用序列号 + * + * @return 序列值 + */ + public static String getId() + { + return getId(commSeqType); + } + + /** + * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串 + * + * @return 序列值 + */ + public static String getId(String type) + { + AtomicInteger atomicInt = commSeq; + if (uploadSeqType.equals(type)) + { + atomicInt = uploadSeq; + } + return getId(atomicInt, 3); + } + + /** + * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串 + * + * @param atomicInt 序列数 + * @param length 数值长度 + * @return 序列值 + */ + public static String getId(AtomicInteger atomicInt, int length) + { + String result = DateUtils.dateTimeNow(); + result += machineCode; + result += getSeq(atomicInt, length); + return result; + } + + /** + * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数 + * + * @return 序列值 + */ + private synchronized static String getSeq(AtomicInteger atomicInt, int length) + { + // 先取值再+1 + int value = atomicInt.getAndIncrement(); + + // 如果更新后值>=10 的 (length)幂次方则重置为1 + int maxSeq = (int) Math.pow(10, length); + if (atomicInt.get() >= maxSeq) + { + atomicInt.set(1); + } + // 转字符串,用0左补齐 + return StringUtils.padl(value, length); + } +} diff --git a/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/uuid/UUID.java b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/uuid/UUID.java new file mode 100644 index 0000000..f63e6e0 --- /dev/null +++ b/idp/backend/idp-starter/src/main/java/org/lingniu/idp/utils/uuid/UUID.java @@ -0,0 +1,485 @@ +package org.lingniu.idp.utils.uuid; + +import org.lingniu.idp.exception.UtilException; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 提供通用唯一识别码(universally unique identifier)(UUID)实现 + * + * @author portal + */ +public final class UUID implements java.io.Serializable, Comparable +{ + private static final long serialVersionUID = -1185015143654744140L; + + /** + * SecureRandom 的单例 + * + */ + private static class Holder + { + static final SecureRandom numberGenerator = getSecureRandom(); + } + + /** 此UUID的最高64有效位 */ + private final long mostSigBits; + + /** 此UUID的最低64有效位 */ + private final long leastSigBits; + + /** + * 私有构造 + * + * @param data 数据 + */ + private UUID(byte[] data) + { + long msb = 0; + long lsb = 0; + assert data.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) + { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) + { + lsb = (lsb << 8) | (data[i] & 0xff); + } + this.mostSigBits = msb; + this.leastSigBits = lsb; + } + + /** + * 使用指定的数据构造新的 UUID。 + * + * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 + * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 + */ + public UUID(long mostSigBits, long leastSigBits) + { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID fastUUID() + { + return randomUUID(false); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID() + { + return randomUUID(true); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID(boolean isSecure) + { + final Random ng = isSecure ? Holder.numberGenerator : getRandom(); + + byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(randomBytes); + } + + /** + * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 + * + * @param name 用于构造 UUID 的字节数组。 + * + * @return 根据指定数组生成的 {@code UUID} + */ + public static UUID nameUUIDFromBytes(byte[] name) + { + MessageDigest md; + try + { + md = MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException nsae) + { + throw new InternalError("MD5 not supported"); + } + byte[] md5Bytes = md.digest(name); + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(md5Bytes); + } + + /** + * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 + * + * @param name 指定 {@code UUID} 字符串 + * @return 具有指定值的 {@code UUID} + * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 + * + */ + public static UUID fromString(String name) + { + String[] components = name.split("-"); + if (components.length != 5) + { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + for (int i = 0; i < 5; i++) + { + components[i] = "0x" + components[i]; + } + + long mostSigBits = Long.decode(components[0]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]).longValue(); + + long leastSigBits = Long.decode(components[3]).longValue(); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]).longValue(); + + return new UUID(mostSigBits, leastSigBits); + } + + /** + * 返回此 UUID 的 128 位值中的最低有效 64 位。 + * + * @return 此 UUID 的 128 位值中的最低有效 64 位。 + */ + public long getLeastSignificantBits() + { + return leastSigBits; + } + + /** + * 返回此 UUID 的 128 位值中的最高有效 64 位。 + * + * @return 此 UUID 的 128 位值中最高有效 64 位。 + */ + public long getMostSignificantBits() + { + return mostSigBits; + } + + /** + * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 + *

+ * 版本号具有以下含意: + *

    + *
  • 1 基于时间的 UUID + *
  • 2 DCE 安全 UUID + *
  • 3 基于名称的 UUID + *
  • 4 随机生成的 UUID + *
+ * + * @return 此 {@code UUID} 的版本号 + */ + public int version() + { + // Version is bits masked by 0x000000000000F000 in MS long + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /** + * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 + *

+ * 变体号具有以下含意: + *

    + *
  • 0 为 NCS 向后兼容保留 + *
  • 2 IETF RFC 4122(Leach-Salz), 用于此类 + *
  • 6 保留,微软向后兼容 + *
  • 7 保留供以后定义使用 + *
+ * + * @return 此 {@code UUID} 相关联的变体号 + */ + public int variant() + { + // This field is composed of a varying number of bits. + // 0 - - Reserved for NCS backward compatibility + // 1 0 - The IETF aka Leach-Salz variant (used by this class) + // 1 1 0 Reserved, Microsoft backward compatibility + // 1 1 1 Reserved for future definition. + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + /** + * 与此 UUID 相关联的时间戳值。 + * + *

+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 + * + *

+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 + */ + public long timestamp() throws UnsupportedOperationException + { + checkTimeBase(); + return (mostSigBits & 0x0FFFL) << 48// + | ((mostSigBits >> 16) & 0x0FFFFL) << 32// + | mostSigBits >>> 32; + } + + /** + * 与此 UUID 相关联的时钟序列值。 + * + *

+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 + *

+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 + * UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的时钟序列 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public int clockSequence() throws UnsupportedOperationException + { + checkTimeBase(); + return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + } + + /** + * 与此 UUID 相关的节点值。 + * + *

+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 + *

+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的节点值 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public long node() throws UnsupportedOperationException + { + checkTimeBase(); + return leastSigBits & 0x0000FFFFFFFFFFFFL; + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @return 此{@code UUID} 的字符串表现形式 + * @see #toString(boolean) + */ + @Override + public String toString() + { + return toString(false); + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 + * @return 此{@code UUID} 的字符串表现形式 + */ + public String toString(boolean isSimple) + { + final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36); + // time_low + builder.append(digits(mostSigBits >> 32, 8)); + if (!isSimple) + { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (!isSimple) + { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (!isSimple) + { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (!isSimple) + { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + @Override + public int hashCode() + { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + /** + * 将此对象与指定对象比较。 + *

+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 + * + * @param obj 要与之比较的对象 + * + * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) + { + if ((null == obj) || (obj.getClass() != UUID.class)) + { + return false; + } + UUID id = (UUID) obj; + return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); + } + + // Comparison Operations + + /** + * 将此 UUID 与指定的 UUID 比较。 + * + *

+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 + * + * @param val 与此 UUID 比较的 UUID + * + * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 + * + */ + @Override + public int compareTo(UUID val) + { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + return (this.mostSigBits < val.mostSigBits ? -1 : // + (this.mostSigBits > val.mostSigBits ? 1 : // + (this.leastSigBits < val.leastSigBits ? -1 : // + (this.leastSigBits > val.leastSigBits ? 1 : // + 0)))); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Private method start + /** + * 返回指定数字对应的hex值 + * + * @param val 值 + * @param digits 位 + * @return 值 + */ + private static String digits(long val, int digits) + { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * 检查是否为time-based版本UUID + */ + private void checkTimeBase() + { + if (version() != 1) + { + throw new UnsupportedOperationException("Not a time-based UUID"); + } + } + + /** + * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) + * + * @return {@link SecureRandom} + */ + public static SecureRandom getSecureRandom() + { + try + { + return SecureRandom.getInstance("SHA1PRNG"); + } + catch (NoSuchAlgorithmException e) + { + throw new UtilException(e); + } + } + + /** + * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + * @return {@link ThreadLocalRandom} + */ + public static ThreadLocalRandom getRandom() + { + return ThreadLocalRandom.current(); + } +} diff --git a/idp/backend/idp-starter/src/main/resources/application.yml b/idp/backend/idp-starter/src/main/resources/application.yml new file mode 100644 index 0000000..9dfbb14 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/application.yml @@ -0,0 +1,115 @@ +# 项目相关配置 +project: + # 名称 + name: idp + # 版本 + version: 1.0.0 + # 版权年份 + copyrightYear: 2026 + # 文件路径 示例( Windows配置D:/portal/uploadPath,Linux配置 /home/portal/uploadPath) + profile: D:/portal/uploadPath + # 获取ip地址开关 + addressEnabled: false + # 验证码类型 math 数字计算 char 字符验证 + captchaType: math +server: + port: 8443 + ssl: + bundle: demo-authorizationserver + client-auth: want + +spring: + profiles: + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + datasource: + url: jdbc:mysql://localhost:3306/portal?useUnicode=true&characterEncoding=utf-8&useSSL=false + username: root + password: Zhang!@# + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + # 连接池名称 + pool-name: MyHikariCP + # 最大连接数,默认为10 + maximum-pool-size: 10 + # 最小空闲连接数 + minimum-idle: 5 + # 连接空闲超时时间(毫秒),默认10分钟 + idle-timeout: 600000 + # 连接最大生命周期(毫秒),默认30分钟 + max-lifetime: 1800000 + # 连接超时时间(毫秒),默认30秒 + connection-timeout: 30000 + # 自动提交事务,默认为true + auto-commit: true + ssl: + bundle: + jks: + demo-authorizationserver: + key: + alias: demo-authorizationserver-sample + password: password + keystore: + location: classpath:keystore.p12 + password: password + type: PKCS12 + truststore: + location: classpath:keystore.p12 + password: password + type: PKCS12 + +# MyBatis Plus配置 +mybatis-plus: + mapper-locations: classpath:mapper/**/*.xml + type-aliases-package: org.lingniu.idp.model.** + configuration: + map-underscore-to-camel-case: true + +logging: + level: + root: info + org.springframework.web: info + org.springframework.security: debug + org.springframework.security.oauth2: debug +# token配置 +token: +# 令牌自定义标识 + header: Idp +# 令牌密钥 + secret: abcdefghijklmnopqrstuvwxyz + accessToken: + # access token令牌有效期(默认30分钟)单位分钟 + expireTime: 30 + # refresh token 令牌有效期(默认7天)单位小时 + refreshToken: + expireTime: 168 + + # 令牌有效期(默认30分钟) + expireTime: 30 +# 用户配置 +user: + password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 +idp: + jwt: + header: Authorization + prefix: "Bearer " + expiration: 1h + refresh-expiration: 7d + rsa: + algorithm: RSA + key-size: 2048 + # 方式1:直接配置密钥内容(适合短的密钥或环境变量) + private-key: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8ILlN6oQR49OQNAx2CmTTvf+NfpniKrrrOSZuOzHDe6BYxehN2wLeUZ9tBCgxBBy7C31qfzNtEByqX3c0wAIgwCfoUA334hjKZTIcBZchhlU8rCUKjwp9Xlo9LltL0fD+bn29N85QxrclFfiW656hXFjl7/E6/CjkJiLcMcqPl9sBnr3eosIFBa2ncEjLgSuoc+62UmuZK8qG5uyujUEK4ih1GMgLFETsY+gfCrEMhpyHATeA6hdsQzH0hQdFMnzUyAQNLid6yTg+vWiuSqaRHXLVoOQ1Id7g9qLS6z5Xq2QgvxhEijyU6dfr3FN2fMs81d2QnelB+XAmbZcNbSjhAgMBAAECggEAA6mvcfDq+V4yNFCPHY0+QVEltJ+OZ3TlhsbqFKNaaK/zg0b3KuzktWdpbznxfmLSHGIw4u3vFmSF6ltJeuFyKSkUndVRME5apTYrL5h8RM7UwgWoAvid/6rinuZUKLnBs3J7SD/Z6IC1+CbhXC7iO8oMgQ2W6MsYELkdXKQriXgm3XtF7fQdZwFw5Rernl0yl5m5hDBnrWbO7e0+9FBTqGyD/qV9OyYBMoRi7K7ZUdJU+y/oNDg6VluuSVO6DifE6eh02jxaKhaFsyl57q9fzofJb/ZA46+eRknI+bU9sSq4gwVMFzB2N16R0kxpOBZYQoi+KgBzjhjZ1ETC8lSpQQKBgQDLckJfdHg/RK8z75JIdM15/KnaQJNyFnlmRIbD4qAeknSYBOYqeanErFBdsXqXcMjn1bXZdwe1ykFwt0PTBB0zOlB8l8lvk1sv4PJu0YrRFyuoRrL+Iw9pDg1Q5AHMewAoOTLstm0T8u8uIuCc7LNqH2b525rk3lwuvwgMUcz+jQKBgQDsuXb4QAaG8LhmwJTUmFgEK1MVrlas6OtNpD8Ua8UD0xOZbxj1t0mHXkD+k7o7Ld4L3HOdaUvMpmrBCX2EIJ3mjvT4EnF6bIIjUFDGeCx5m7s6KEUzhlV6QaVBLqr4M0HEyLB2FztfPLSbKnD52VKJ3dV4bELnykD7XHG8q6d4pQKBgQCD1VK1UF/rf7KY0RHV6fqMpfHbACWLtIyOfLFnlh90MCtlpycPNy+Pxql9TVjHccp+kLn6ZWuVna6yP8+vmebiH1OwqRtbNf3NFNOEhDyUKZOcw9ORY17FENoIJPgVbU84wXgCdGRSnQXou6kZhzjr99Ve67N/w6ewkxHACfHwsQKBgHuXOO3TP5UkCKJc2VdFUm6az/35z3bnBDK9FdHrkii1Av6Qak+fKdxq8TP4nLpY8BzxM8tzNgfautGdytI41TeSW4NI3cY08JzPSdzU0SOMkuuCKt8Du4zgyQ6G9uwulp/Ox5Jf/rdyUjjQp7tKIzWng9QjcarihZq2YQtH81+hAoGADWhSLTU9Dt9XM82Yc0WC4Rl0EwPV+mH32CDKC8xIVMTGodc0JyKIyoykpml2U827PIHhxms4CoXTzxcBaxt3dAfezqvmSzrMW2LA3Xi7pIGJqNtk16wb38XrLqxZZGYA1DWUqMvp/GniM7qTSF7aHzjHzX4ZXL6jmi3tqJtivfw= + -----END PRIVATE KEY----- + public-key: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvCC5TeqEEePTkDQMdgpk073/jX6Z4iq66zkmbjsxw3ugWMXoTdsC3lGfbQQoMQQcuwt9an8zbRAcql93NMACIMAn6FAN9+IYymUyHAWXIYZVPKwlCo8KfV5aPS5bS9Hw/m59vTfOUMa3JRX4luueoVxY5e/xOvwo5CYi3DHKj5fbAZ693qLCBQWtp3BIy4ErqHPutlJrmSvKhubsro1BCuIodRjICxRE7GPoHwqxDIachwE3gOoXbEMx9IUHRTJ81MgEDS4nesk4Pr1orkqmkR1y1aDkNSHe4Pai0us+V6tkIL8YRIo8lOnX69xTdnzLPNXdkJ3pQflwJm2XDW0o4QIDAQAB + -----END PUBLIC KEY----- \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/auth.sql b/idp/backend/idp-starter/src/main/resources/auth.sql new file mode 100644 index 0000000..b482a46 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/auth.sql @@ -0,0 +1,54 @@ +CREATE TABLE oauth2_authorization ( + id varchar(100) NOT NULL COMMENT '授权记录的唯一标识符', + registered_client_id varchar(100) NOT NULL COMMENT '注册客户端的ID,关联到客户端注册表', + principal_name varchar(200) NOT NULL COMMENT '授权主体的名称(通常是用户ID或用户名)', + authorization_grant_type varchar(100) NOT NULL COMMENT '授权类型(如:authorization_code, client_credentials, refresh_token等)', + authorized_scopes varchar(1000) DEFAULT NULL COMMENT '已授权的范围(用空格分隔的scope列表)', + attributes blob DEFAULT NULL COMMENT '附加属性,以二进制格式存储', + state varchar(500) DEFAULT NULL COMMENT 'OAuth2状态参数,用于防止CSRF攻击', + authorization_code_value blob DEFAULT NULL COMMENT '授权码的值(加密存储)', + authorization_code_issued_at timestamp NULL DEFAULT NULL COMMENT '授权码的颁发时间', + authorization_code_expires_at timestamp NULL DEFAULT NULL COMMENT '授权码的过期时间', + authorization_code_metadata blob DEFAULT NULL COMMENT '授权码的元数据', + access_token_value blob DEFAULT NULL COMMENT '访问令牌的值(加密存储)', + access_token_issued_at timestamp NULL DEFAULT NULL COMMENT '访问令牌的颁发时间', + access_token_expires_at timestamp NULL DEFAULT NULL COMMENT '访问令牌的过期时间', + access_token_metadata blob DEFAULT NULL COMMENT '访问令牌的元数据', + access_token_type varchar(100) DEFAULT NULL COMMENT '访问令牌类型(如:Bearer)', + access_token_scopes varchar(1000) DEFAULT NULL COMMENT '访问令牌的有效范围', + oidc_id_token_value blob DEFAULT NULL COMMENT 'OIDC ID令牌的值(加密存储)', + oidc_id_token_issued_at timestamp NULL DEFAULT NULL COMMENT 'OIDC ID令牌的颁发时间', + oidc_id_token_expires_at timestamp NULL DEFAULT NULL COMMENT 'OIDC ID令牌的过期时间', + oidc_id_token_metadata blob DEFAULT NULL COMMENT 'OIDC ID令牌的元数据', + refresh_token_value blob DEFAULT NULL COMMENT '刷新令牌的值(加密存储)', + refresh_token_issued_at timestamp NULL DEFAULT NULL COMMENT '刷新令牌的颁发时间', + refresh_token_expires_at timestamp NULL DEFAULT NULL COMMENT '刷新令牌的过期时间', + refresh_token_metadata blob DEFAULT NULL COMMENT '刷新令牌的元数据', + user_code_value blob DEFAULT NULL COMMENT '设备流用户码的值(加密存储)', + user_code_issued_at timestamp NULL DEFAULT NULL COMMENT '设备流用户码的颁发时间', + user_code_expires_at timestamp NULL DEFAULT NULL COMMENT '设备流用户码的过期时间', + user_code_metadata blob DEFAULT NULL COMMENT '设备流用户码的元数据', + device_code_value blob DEFAULT NULL COMMENT '设备流设备码的值(加密存储)', + device_code_issued_at timestamp NULL DEFAULT NULL COMMENT '设备流设备码的颁发时间', + device_code_expires_at timestamp NULL DEFAULT NULL COMMENT '设备流设备码的过期时间', + device_code_metadata blob DEFAULT NULL COMMENT '设备流设备码的元数据', + PRIMARY KEY (id) +) COMMENT='OAuth2 授权表,存储所有OAuth2和OpenID Connect的授权信息'; + +CREATE INDEX idx_registered_client_id ON oauth2_authorization(registered_client_id); +CREATE INDEX idx_principal_name ON oauth2_authorization(principal_name); +CREATE INDEX idx_state ON oauth2_authorization(state); +CREATE INDEX idx_access_token_expires_at ON oauth2_authorization(access_token_expires_at); +CREATE INDEX idx_refresh_token_expires_at ON oauth2_authorization(refresh_token_expires_at); +CREATE INDEX idx_authorization_code_expires_at ON oauth2_authorization(authorization_code_expires_at); + + +CREATE TABLE oauth2_authorization_consent ( + registered_client_id varchar(100) NOT NULL COMMENT '注册客户端的ID,关联到客户端注册表', + principal_name varchar(200) NOT NULL COMMENT '授权主体的名称(通常是用户ID或用户名)', + authorities varchar(1000) NOT NULL COMMENT '已授予的权限列表(逗号分隔的权限字符串)', + PRIMARY KEY (registered_client_id, principal_name) +) COMMENT='OAuth2授权同意表,存储用户对客户端的授权同意记录'; + +CREATE INDEX idx_oauth2_authorization_consent_principal_name ON oauth2_authorization_consent(principal_name); +CREATE INDEX idx_oauth2_authorization_consent_client_id ON oauth2_authorization_consent(registered_client_id); \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/i18n/messages.properties b/idp/backend/idp-starter/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..93de005 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/i18n/messages.properties @@ -0,0 +1,38 @@ +#错误消息 +not.null=* 必须填写 +user.jcaptcha.error=验证码错误 +user.jcaptcha.expire=验证码已失效 +user.not.exists=用户不存在/密码错误 +user.password.not.match=用户不存在/密码错误 +user.password.retry.limit.count=密码输入错误{0}次 +user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 +user.password.delete=对不起,您的账号已被删除 +user.blocked=用户已封禁,请联系管理员 +role.blocked=角色已封禁,请联系管理员 +login.blocked=很遗憾,访问IP已被列入系统黑名单 +user.logout.success=退出成功 + +length.not.valid=长度必须在{min}到{max}个字符之间 + +user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头 +user.password.not.valid=* 5-50个字符 + +user.email.not.valid=邮箱格式错误 +user.mobile.phone.number.not.valid=手机号格式错误 +user.login.success=登录成功 +user.register.success=注册成功 +user.notfound=请重新登录 +user.forcelogout=管理员强制退出,请重新登录 +user.unknown.error=未知错误,请重新登录 + +##文件上传消息 +upload.exceed.maxSize=上传的文件大小超出限制的文件大小!
允许的文件最大大小是:{0}MB! +upload.filename.exceed.length=上传的文件名最长{0}个字符 + +##权限 +no.permission=您没有数据的权限,请联系管理员添加权限 [{0}] +no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}] +no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}] +no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}] +no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}] +no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}] diff --git a/idp/backend/idp-starter/src/main/resources/keystore.p12 b/idp/backend/idp-starter/src/main/resources/keystore.p12 new file mode 100644 index 0000000..ec191fa Binary files /dev/null and b/idp/backend/idp-starter/src/main/resources/keystore.p12 differ diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysConfigMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysConfigMapper.xml new file mode 100644 index 0000000..2fe7764 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysConfigMapper.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark + from sys_config + + + + + + + and config_id = #{configId} + + + and config_key = #{configKey} + + + + + + + + + + + + + + insert into sys_config ( + config_name, + config_key, + config_value, + config_type, + create_by, + remark, + create_time + )values( + #{configName}, + #{configKey}, + #{configValue}, + #{configType}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_config + + config_name = #{configName}, + config_key = #{configKey}, + config_value = #{configValue}, + config_type = #{configType}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where config_id = #{configId} + + + + delete from sys_config where config_id = #{configId} + + + + delete from sys_config where config_id in + + #{configId} + + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysDeptMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysDeptMapper.xml new file mode 100644 index 0000000..a3e0266 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysDeptMapper.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time + from sys_dept d + + + + + + + + + + + + + + + + + + + + + insert into sys_dept( + dept_id, + parent_id, + dept_name, + ancestors, + order_num, + leader, + phone, + email, + status, + create_by, + create_time + )values( + #{deptId}, + #{parentId}, + #{deptName}, + #{ancestors}, + #{orderNum}, + #{leader}, + #{phone}, + #{email}, + #{status}, + #{createBy}, + sysdate() + ) + + + + update sys_dept + + parent_id = #{parentId}, + dept_name = #{deptName}, + ancestors = #{ancestors}, + order_num = #{orderNum}, + leader = #{leader}, + phone = #{phone}, + email = #{email}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where dept_id = #{deptId} + + + + update sys_dept set ancestors = + + when #{item.deptId} then #{item.ancestors} + + where dept_id in + + #{item.deptId} + + + + + update sys_dept set status = '0' where dept_id in + + #{deptId} + + + + + update sys_dept set del_flag = '2' where dept_id = #{deptId} + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysLogininforMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysLogininforMapper.xml new file mode 100644 index 0000000..a4db19e --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysLogininforMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time) + values (#{userName}, #{status}, #{ipaddr}, #{loginLocation}, #{browser}, #{os}, #{msg}, sysdate()) + + + + + + delete from sys_logininfor where info_id in + + #{infoId} + + + + + truncate table sys_logininfor + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysMenuMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysMenuMapper.xml new file mode 100644 index 0000000..e339964 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysMenuMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time + from sys_menu + + + + + + + + + + + + + + + + + + + + + + + + + + update sys_menu + + menu_name = #{menuName}, + parent_id = #{parentId}, + order_num = #{orderNum}, + path = #{path}, + component = #{component}, + `query` = #{query}, + route_name = #{routeName}, + is_frame = #{isFrame}, + is_cache = #{isCache}, + menu_type = #{menuType}, + visible = #{visible}, + status = #{status}, + perms = #{perms}, + icon = #{icon}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where menu_id = #{menuId} + + + + insert into sys_menu( + menu_id, + parent_id, + menu_name, + order_num, + path, + component, + `query`, + route_name, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + remark, + create_by, + create_time + )values( + #{menuId}, + #{parentId}, + #{menuName}, + #{orderNum}, + #{path}, + #{component}, + #{query}, + #{routeName}, + #{isFrame}, + #{isCache}, + #{menuType}, + #{visible}, + #{status}, + #{perms}, + #{icon}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_menu where menu_id = #{menuId} + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysOperLogMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysOperLogMapper.xml new file mode 100644 index 0000000..9c4a355 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysOperLogMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time, cost_time + from sys_oper_log + + + + insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, cost_time, oper_time) + values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operLocation}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, #{costTime}, sysdate()) + + + + + + delete from sys_oper_log where oper_id in + + #{operId} + + + + + + + truncate table sys_oper_log + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysPostMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysPostMapper.xml new file mode 100644 index 0000000..ec67ec1 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysPostMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + select post_id, post_code, post_name, post_sort, status, create_by, create_time, remark + from sys_post + + + + + + + + + + + + + + + + + + update sys_post + + post_code = #{postCode}, + post_name = #{postName}, + post_sort = #{postSort}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where post_id = #{postId} + + + + insert into sys_post( + post_id, + post_code, + post_name, + post_sort, + status, + remark, + create_by, + create_time + )values( + #{postId}, + #{postCode}, + #{postName}, + #{postSort}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_post where post_id = #{postId} + + + + delete from sys_post where post_id in + + #{postId} + + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysRoleDeptMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysRoleDeptMapper.xml new file mode 100644 index 0000000..f0ce5f0 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysRoleDeptMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_role_dept where role_id=#{roleId} + + + + + + delete from sys_role_dept where role_id in + + #{roleId} + + + + + insert into sys_role_dept(role_id, dept_id) values + + (#{item.roleId},#{item.deptId}) + + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysRoleMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysRoleMapper.xml new file mode 100644 index 0000000..2c4c854 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysRoleMapper.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly, + r.status, r.del_flag, r.create_time, r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join sys_user u on u.user_id = ur.user_id + left join sys_dept d on u.dept_id = d.dept_id + + + + + + + + + + + + + + + + + + + + insert into sys_role( + role_id, + role_name, + role_key, + role_sort, + data_scope, + menu_check_strictly, + dept_check_strictly, + status, + remark, + create_by, + create_time + )values( + #{roleId}, + #{roleName}, + #{roleKey}, + #{roleSort}, + #{dataScope}, + #{menuCheckStrictly}, + #{deptCheckStrictly}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_role + + role_name = #{roleName}, + role_key = #{roleKey}, + role_sort = #{roleSort}, + data_scope = #{dataScope}, + menu_check_strictly = #{menuCheckStrictly}, + dept_check_strictly = #{deptCheckStrictly}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id in + + #{roleId} + + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysRoleMenuMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysRoleMenuMapper.xml new file mode 100644 index 0000000..45abc03 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysRoleMenuMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + delete from sys_role_menu where role_id=#{roleId} + + + + delete from sys_role_menu where role_id in + + #{roleId} + + + + + insert into sys_role_menu(role_id, menu_id) values + + (#{item.roleId},#{item.menuId}) + + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysUserMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysUserMapper.xml new file mode 100644 index 0000000..ad2339e --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysUserMapper.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark, + d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, + p.post_id,p.post_code,p.post_name,p.post_sort,p.remark,p.`status` + from sys_user u + left join sys_user_dept sud on sud.user_id = u.user_id + left join sys_dept d on sud.dept_id = d.dept_id + left join sys_user_post sup on sup.user_id= u.user_id + left join sys_post p on p.post_id = sup.post_id + + + + + + + + + + + + + + + + + insert into sys_user( + user_id, + dept_id, + user_name, + nick_name, + email, + avatar, + phonenumber, + sex, + password, + status, + pwd_update_date, + create_by, + remark, + create_time + )values( + #{userId}, + #{deptId}, + #{userName}, + #{nickName}, + #{email}, + #{avatar}, + #{phonenumber}, + #{sex}, + #{password}, + #{status}, + #{pwdUpdateDate}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_user + + dept_id = #{deptId}, + nick_name = #{nickName}, + email = #{email}, + phonenumber = #{phonenumber}, + sex = #{sex}, + avatar = #{avatar}, + password = #{password}, + status = #{status}, + login_ip = #{loginIp}, + login_date = #{loginDate}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where user_id = #{userId} + + + + update sys_user set status = #{status}, update_time = sysdate() where user_id = #{userId} + + + + update sys_user set avatar = #{avatar}, update_time = sysdate() where user_id = #{userId} + + + + update sys_user set login_ip = #{loginIp}, login_date = #{loginDate} where user_id = #{userId} + + + + update sys_user set pwd_update_date = sysdate(), password = #{password}, update_time = sysdate() where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id in + + #{userId} + + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysUserPostMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysUserPostMapper.xml new file mode 100644 index 0000000..101910d --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysUserPostMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_user_post where user_id=#{userId} + + + + + + delete from sys_user_post where user_id in + + #{userId} + + + + + insert into sys_user_post(user_id, post_id) values + + (#{item.userId},#{item.postId}) + + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/main/resources/mapper/SysUserRoleMapper.xml b/idp/backend/idp-starter/src/main/resources/mapper/SysUserRoleMapper.xml new file mode 100644 index 0000000..c80a479 --- /dev/null +++ b/idp/backend/idp-starter/src/main/resources/mapper/SysUserRoleMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + delete from sys_user_role where user_id=#{userId} + + + + + + delete from sys_user_role where user_id in + + #{userId} + + + + + insert into sys_user_role(user_id, role_id) values + + (#{item.userId},#{item.roleId}) + + + + + delete from sys_user_role where user_id=#{userId} and role_id=#{roleId} + + + + delete from sys_user_role where role_id=#{roleId} and user_id in + + #{userId} + + + \ No newline at end of file diff --git a/idp/backend/idp-starter/src/test/java/org/lingniu/idp/IdpStarterApplicationTests.java b/idp/backend/idp-starter/src/test/java/org/lingniu/idp/IdpStarterApplicationTests.java new file mode 100644 index 0000000..16b2df2 --- /dev/null +++ b/idp/backend/idp-starter/src/test/java/org/lingniu/idp/IdpStarterApplicationTests.java @@ -0,0 +1,13 @@ +package org.lingniu.idp; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class IdpStarterApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/idp/frontend/.env.development b/idp/frontend/.env.development new file mode 100644 index 0000000..130999b --- /dev/null +++ b/idp/frontend/.env.development @@ -0,0 +1,10 @@ +# 页面标题 +VITE_APP_TITLE = IDP统一登录系统 + +# 开发环境配置 +VITE_APP_ENV = 'development' + +# 开发环境 +VITE_APP_BASE_API = '/dev-api' + +VITE_APP_DEFAULT_PAGE='' diff --git a/idp/frontend/.env.production b/idp/frontend/.env.production new file mode 100644 index 0000000..417f85c --- /dev/null +++ b/idp/frontend/.env.production @@ -0,0 +1,13 @@ +# 页面标题 +VITE_APP_TITLE = IDP统一登录系统 + +# 生产环境配置 +VITE_APP_ENV = 'production' + +# 生产环境 +VITE_APP_BASE_API = '/prod-api' + +# 是否在打包时开启压缩,支持 gzip 和 brotli +VITE_BUILD_COMPRESS = gzip + +VITE_APP_DEFAULT_PAGE='' \ No newline at end of file diff --git a/idp/frontend/.env.staging b/idp/frontend/.env.staging new file mode 100644 index 0000000..cdc6bff --- /dev/null +++ b/idp/frontend/.env.staging @@ -0,0 +1,13 @@ +# 页面标题 +VITE_APP_TITLE = IDP统一登录系统 + +# 生产环境配置 +VITE_APP_ENV = 'staging' + +# 生产环境 +VITE_APP_BASE_API = '/stage-api' + +# 是否在打包时开启压缩,支持 gzip 和 brotli +VITE_BUILD_COMPRESS = gzip + +VITE_APP_DEFAULT_PAGE='' \ No newline at end of file diff --git a/idp/frontend/crypto-js.d.ts b/idp/frontend/crypto-js.d.ts new file mode 100644 index 0000000..bca7d6c --- /dev/null +++ b/idp/frontend/crypto-js.d.ts @@ -0,0 +1,26 @@ +declare module 'crypto-js' { + export const MD5: (message: string) => string; + export const SHA256: (message: string) => string; + export const AES: { + encrypt: (message: string, key: string) => { + toString: () => string; + }; + decrypt: (ciphertext: string, key: string) => { + toString: (encoding: 'utf8') => string; + }; + }; + export const enc: { + Utf8: { + parse: (str: string) => any; + stringify: (wordArray: any) => string; + }; + Hex: { + parse: (str: string) => any; + stringify: (wordArray: any) => string; + }; + Base64: { + parse: (str: string) => any; + stringify: (wordArray: any) => string; + }; + }; +} \ No newline at end of file diff --git a/idp/frontend/index.html b/idp/frontend/index.html new file mode 100644 index 0000000..5cab8aa --- /dev/null +++ b/idp/frontend/index.html @@ -0,0 +1,215 @@ + + + + + + + + + + %VITE_APP_TITLE% + + + + + +

+
+
+
+
+
正在加载系统资源,请耐心等待
+
+
+ + + + \ No newline at end of file diff --git a/idp/frontend/package.json b/idp/frontend/package.json new file mode 100644 index 0000000..e122584 --- /dev/null +++ b/idp/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "unified-login-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "1.13.4", + "crypto-js": "^4.2.0", + "element-plus": "^2.6.0", + "js-cookie": "3.0.5", + "jsencrypt": "3.3.2", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@types/js-cookie": "^3.0.6", + "@vitejs/plugin-vue": "6.0.3", + "sass-embedded": "^1.97.3", + "typescript": "^5.9.3", + "unplugin-auto-import": "0.18.6", + "unplugin-vue-setup-extend-plus": "1.0.1", + "vite": "6.4.1", + "vite-plugin-compression": "0.5.1", + "vite-plugin-svg-icons": "2.0.1", + "vue-tsc": "^3.2.4", + "web-storage-cache": "^1.1.1" + } +} \ No newline at end of file diff --git a/idp/frontend/pnpm-lock.yaml b/idp/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..4e7cf92 --- /dev/null +++ b/idp/frontend/pnpm-lock.yaml @@ -0,0 +1,4326 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.2 + version: 2.3.2(vue@3.5.27(typescript@5.9.3)) + axios: + specifier: 1.13.4 + version: 1.13.4 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + element-plus: + specifier: ^2.6.0 + version: 2.13.1(vue@3.5.27(typescript@5.9.3)) + js-cookie: + specifier: 3.0.5 + version: 3.0.5 + jsencrypt: + specifier: 3.3.2 + version: 3.3.2 + pinia: + specifier: ^2.1.7 + version: 2.3.1(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)) + vue: + specifier: ^3.4.21 + version: 3.5.27(typescript@5.9.3) + vue-router: + specifier: ^4.3.0 + version: 4.6.4(vue@3.5.27(typescript@5.9.3)) + devDependencies: + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@vitejs/plugin-vue': + specifier: 6.0.3 + version: 6.0.3(vite@6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3))(vue@3.5.27(typescript@5.9.3)) + sass-embedded: + specifier: ^1.97.3 + version: 1.97.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + unplugin-auto-import: + specifier: 0.18.6 + version: 0.18.6(@vueuse/core@10.11.1(vue@3.5.27(typescript@5.9.3)))(rollup@4.56.0) + unplugin-vue-setup-extend-plus: + specifier: 1.0.1 + version: 1.0.1 + vite: + specifier: 6.4.1 + version: 6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3) + vite-plugin-compression: + specifier: 0.5.1 + version: 0.5.1(vite@6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3)) + vite-plugin-svg-icons: + specifier: 2.0.1 + version: 2.0.1(vite@6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3)) + vue-tsc: + specifier: ^3.2.4 + version: 3.2.4(typescript@5.9.3) + web-storage-cache: + specifier: ^1.1.1 + version: 1.1.1 + +packages: + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@bufbuild/protobuf@2.11.0': + resolution: {integrity: sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==} + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.56.0': + resolution: {integrity: sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.56.0': + resolution: {integrity: sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.56.0': + resolution: {integrity: sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.56.0': + resolution: {integrity: sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.56.0': + resolution: {integrity: sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.56.0': + resolution: {integrity: sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.56.0': + resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.56.0': + resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.56.0': + resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.56.0': + resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.56.0': + resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.56.0': + resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.56.0': + resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.56.0': + resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.56.0': + resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.56.0': + resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.56.0': + resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.56.0': + resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.56.0': + resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.56.0': + resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.56.0': + resolution: {integrity: sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.56.0': + resolution: {integrity: sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.56.0': + resolution: {integrity: sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.56.0': + resolution: {integrity: sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.56.0': + resolution: {integrity: sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==} + cpu: [x64] + os: [win32] + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.23': + resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} + + '@types/node@25.0.10': + resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==} + + '@types/svgo@2.6.4': + resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@vitejs/plugin-vue@6.0.3': + resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.2.25 + + '@volar/language-core@2.4.27': + resolution: {integrity: sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==} + + '@volar/source-map@2.4.27': + resolution: {integrity: sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==} + + '@volar/typescript@2.4.27': + resolution: {integrity: sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==} + + '@vue/compiler-core@3.5.27': + resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==} + + '@vue/compiler-dom@3.5.27': + resolution: {integrity: sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==} + + '@vue/compiler-sfc@3.5.27': + resolution: {integrity: sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==} + + '@vue/compiler-ssr@3.5.27': + resolution: {integrity: sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/language-core@3.2.4': + resolution: {integrity: sha512-bqBGuSG4KZM45KKTXzGtoCl9cWju5jsaBKaJJe3h5hRAAWpZUuj5G+L+eI01sPIkm4H6setKRlw7E85wLdDNew==} + + '@vue/reactivity@3.5.27': + resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==} + + '@vue/runtime-core@3.5.27': + resolution: {integrity: sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==} + + '@vue/runtime-dom@3.5.27': + resolution: {integrity: sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==} + + '@vue/server-renderer@3.5.27': + resolution: {integrity: sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==} + peerDependencies: + vue: 3.5.27 + + '@vue/shared@3.5.27': + resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==} + + '@vueuse/core@10.11.1': + resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} + + '@vueuse/metadata@10.11.1': + resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} + + '@vueuse/shared@10.11.1': + resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + alien-signals@3.1.2: + resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + + ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + + ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + + arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} + + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@1.13.4: + resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} + + big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@2.3.2: + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorjs.io@0.5.2: + resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} + + define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + + define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-serializer@0.2.2: + resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + domelementtype@1.3.1: + resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@2.4.2: + resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domutils@1.7.0: + resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + element-plus@2.13.1: + resolution: {integrity: sha512-eG4BDBGdAsUGN6URH1PixzZb0ngdapLivIk1meghS1uEueLvQ3aljSKrCt5x6sYb6mUk8eGtzTQFgsPmLavQcA==} + peerDependencies: + vue: ^3.3.0 + + emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + + entities@1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + expand-brackets@2.1.4: + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + + extglob@2.0.4: + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@1.0.0: + resolution: {integrity: sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==} + engines: {node: '>=0.10.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} + + has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + + has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} + + has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + htmlparser2@3.10.1: + resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} + + image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-accessor-descriptor@1.0.1: + resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} + engines: {node: '>= 0.10'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-descriptor@0.1.7: + resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} + engines: {node: '>= 0.4'} + + is-descriptor@1.0.3: + resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==} + engines: {node: '>= 0.4'} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + js-base64@2.6.4: + resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==} + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsencrypt@3.3.2: + resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} + + kind-of@5.1.0: + resolution: {integrity: sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==} + engines: {node: '>=0.10.0'} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + loader-utils@1.4.2: + resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} + engines: {node: '>=4.0.0'} + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + magic-string@0.26.7: + resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + + map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + merge-options@1.0.1: + resolution: {integrity: sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg==} + engines: {node: '>=4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@3.1.0: + resolution: {integrity: sha512-3StSelAE+hnRvMs8IdVW7Uhk8CVed5tp+kLLGlBP6WiRAXS21GPGu/Nat4WNPXj2Eoc24B02SaeoyozPMfj0/g==} + engines: {node: '>=0.10.0'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanomatch@1.2.13: + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + pathe@0.2.0: + resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pinia@2.3.1: + resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-prefix-selector@1.16.1: + resolution: {integrity: sha512-Umxu+FvKMwlY6TyDzGFoSUnzW+NOfMBLyC1tAkIjgX+Z/qGspJeRjVC903D7mx7TuBpJlwti2ibXtWuA7fKMeQ==} + peerDependencies: + postcss: '>4 <9' + + postcss@5.2.18: + resolution: {integrity: sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==} + engines: {node: '>=0.12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + posthtml-parser@0.2.1: + resolution: {integrity: sha512-nPC53YMqJnc/+1x4fRYFfm81KV2V+G9NZY+hTohpYg64Ay7NemWWcV4UWuy/SgMupqQ3kJ88M/iRfZmSnxT+pw==} + + posthtml-rename-id@1.0.12: + resolution: {integrity: sha512-UKXf9OF/no8WZo9edRzvuMenb6AD5hDLzIepJW+a4oJT+T/Lx7vfMYWT4aWlGNQh0WMhnUx1ipN9OkZ9q+ddEw==} + + posthtml-render@1.4.0: + resolution: {integrity: sha512-W1779iVHGfq0Fvh2PROhCe2QhB8mEErgqzo1wpIt36tCgChafP+hbXIhLDOM8ePJrZcFs0vkNEtdibEWVqChqw==} + engines: {node: '>=10'} + + posthtml-svg-mode@1.0.3: + resolution: {integrity: sha512-hEqw9NHZ9YgJ2/0G7CECOeuLQKZi8HjWLkBaSVtOWjygQ9ZD8P7tqeowYs7WrFdKsWEKG7o+IlsPY8jrr0CJpQ==} + + posthtml@0.9.2: + resolution: {integrity: sha512-spBB5sgC4cv2YcW03f/IAUN1pgDJWNWD8FzkyY4mArLUMJW+KlQhlmUdKAHQuPfb00Jl5xIfImeOsf6YL8QK7Q==} + engines: {node: '>=0.10.0'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + query-string@4.3.4: + resolution: {integrity: sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==} + engines: {node: '>=0.10.0'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated + + ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.56.0: + resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + + sass-embedded-all-unknown@1.97.3: + resolution: {integrity: sha512-t6N46NlPuXiY3rlmG6/+1nwebOBOaLFOOVqNQOC2cJhghOD4hh2kHNQQTorCsbY9S1Kir2la1/XLBwOJfui0xg==} + cpu: ['!arm', '!arm64', '!riscv64', '!x64'] + + sass-embedded-android-arm64@1.97.3: + resolution: {integrity: sha512-aiZ6iqiHsUsaDx0EFbbmmA0QgxicSxVVN3lnJJ0f1RStY0DthUkquGT5RJ4TPdaZ6ebeJWkboV4bra+CP766eA==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [android] + + sass-embedded-android-arm@1.97.3: + resolution: {integrity: sha512-cRTtf/KV/q0nzGZoUzVkeIVVFv3L/tS1w4WnlHapphsjTXF/duTxI8JOU1c/9GhRPiMdfeXH7vYNcMmtjwX7jg==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [android] + + sass-embedded-android-riscv64@1.97.3: + resolution: {integrity: sha512-zVEDgl9JJodofGHobaM/q6pNETG69uuBIGQHRo789jloESxxZe82lI3AWJQuPmYCOG5ElfRthqgv89h3gTeLYA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [android] + + sass-embedded-android-x64@1.97.3: + resolution: {integrity: sha512-3ke0le7ZKepyXn/dKKspYkpBC0zUk/BMciyP5ajQUDy4qJwobd8zXdAq6kOkdiMB+d9UFJOmEkvgFJHl3lqwcw==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [android] + + sass-embedded-darwin-arm64@1.97.3: + resolution: {integrity: sha512-fuqMTqO4gbOmA/kC5b9y9xxNYw6zDEyfOtMgabS7Mz93wimSk2M1quQaTJnL98Mkcsl2j+7shNHxIS/qpcIDDA==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [darwin] + + sass-embedded-darwin-x64@1.97.3: + resolution: {integrity: sha512-b/2RBs/2bZpP8lMkyZ0Px0vkVkT8uBd0YXpOwK7iOwYkAT8SsO4+WdVwErsqC65vI5e1e5p1bb20tuwsoQBMVA==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [darwin] + + sass-embedded-linux-arm64@1.97.3: + resolution: {integrity: sha512-IP1+2otCT3DuV46ooxPaOKV1oL5rLjteRzf8ldZtfIEcwhSgSsHgA71CbjYgLEwMY9h4jeal8Jfv3QnedPvSjg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + libc: glibc + + sass-embedded-linux-arm@1.97.3: + resolution: {integrity: sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + libc: glibc + + sass-embedded-linux-musl-arm64@1.97.3: + resolution: {integrity: sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + libc: musl + + sass-embedded-linux-musl-arm@1.97.3: + resolution: {integrity: sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + libc: musl + + sass-embedded-linux-musl-riscv64@1.97.3: + resolution: {integrity: sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + libc: musl + + sass-embedded-linux-musl-x64@1.97.3: + resolution: {integrity: sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + libc: musl + + sass-embedded-linux-riscv64@1.97.3: + resolution: {integrity: sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + libc: glibc + + sass-embedded-linux-x64@1.97.3: + resolution: {integrity: sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + libc: glibc + + sass-embedded-unknown-all@1.97.3: + resolution: {integrity: sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q==} + os: ['!android', '!darwin', '!linux', '!win32'] + + sass-embedded-win32-arm64@1.97.3: + resolution: {integrity: sha512-RDGtRS1GVvQfMGAmVXNxYiUOvPzn9oO1zYB/XUM9fudDRnieYTcUytpNTQZLs6Y1KfJxgt5Y+giRceC92fT8Uw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [win32] + + sass-embedded-win32-x64@1.97.3: + resolution: {integrity: sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [win32] + + sass-embedded@1.97.3: + resolution: {integrity: sha512-eKzFy13Nk+IRHhlAwP3sfuv+PzOrvzUkwJK2hdoCKYcWGSdmwFpeGpWmyewdw8EgBnsKaSBtgf/0b2K635ecSA==} + engines: {node: '>=16.0.0'} + hasBin: true + + sass@1.97.3: + resolution: {integrity: sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==} + engines: {node: '>=14.0.0'} + hasBin: true + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + + snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + + snapdragon@0.8.2: + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + + source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + + stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + + static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + strict-uri-encode@1.1.0: + resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} + engines: {node: '>=0.10.0'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + + supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + + supports-color@3.2.3: + resolution: {integrity: sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==} + engines: {node: '>=0.8.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + svg-baker@1.7.0: + resolution: {integrity: sha512-nibslMbkXOIkqKVrfcncwha45f97fGuAOn1G99YwnwTj8kF9YiM6XexPcUso97NxOm6GsP0SIvYVIosBis1xLg==} + + svgo@2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} + hasBin: true + + sync-child-process@1.0.2: + resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} + engines: {node: '>=16.0.0'} + + sync-message-port@1.1.3: + resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==} + engines: {node: '>=16.0.0'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + + to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} + + traverse@0.6.11: + resolution: {integrity: sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==} + engines: {node: '>= 0.4'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typedarray.prototype.slice@1.0.5: + resolution: {integrity: sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unimport@3.14.6: + resolution: {integrity: sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==} + + union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin-auto-import@0.18.6: + resolution: {integrity: sha512-LMFzX5DtkTj/3wZuyG5bgKBoJ7WSgzqSGJ8ppDRdlvPh45mx6t6w3OcbExQi53n3xF5MYkNGPNR/HYOL95KL2A==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^3.2.2 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + + unplugin-vue-setup-extend-plus@1.0.1: + resolution: {integrity: sha512-mW2IzkyJITyspAV/LEdnEyE1CJip9jB5fCeaVv7Q6X0oJyDrOxXoB+jyet0q5pRJNjErbjQx950/8NPTvbqLTQ==} + + unplugin@1.16.1: + resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} + engines: {node: '>=14.0.0'} + + unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} + + urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated + + use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite-plugin-compression@0.5.1: + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-svg-icons@2.0.1: + resolution: {integrity: sha512-6ktD+DhV6Rz3VtedYvBKKVA2eXF+sAQVaKkKLDSqGUfnhqXl3bj5PPkVTl3VexfTuZy66PmINi8Q6eFnVfRUmA==} + peerDependencies: + vite: '>=2.0.0' + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-tsc@3.2.4: + resolution: {integrity: sha512-xj3YCvSLNDKt1iF9OcImWHhmYcihVu9p4b9s4PGR/qp6yhW+tZJaypGxHScRyOrdnHvaOeF+YkZOdKwbgGvp5g==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.27: + resolution: {integrity: sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + web-storage-cache@1.1.1: + resolution: {integrity: sha512-D0MieGooOs8RpsrK+vnejXnvh4OOv/+lTFB35JRkJJQt+uOjPE08XpaE0QBLMTRu47B1KGT/Nq3Gbag3Orinzw==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + +snapshots: + + '@antfu/utils@0.7.10': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bufbuild/protobuf@2.11.0': {} + + '@ctrl/tinycolor@3.6.1': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.27(typescript@5.9.3))': + dependencies: + vue: 3.5.27(typescript@5.9.3) + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.3 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@rollup/pluginutils@5.3.0(rollup@4.56.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.56.0 + + '@rollup/rollup-android-arm-eabi@4.56.0': + optional: true + + '@rollup/rollup-android-arm64@4.56.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.56.0': + optional: true + + '@rollup/rollup-darwin-x64@4.56.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.56.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.56.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.56.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.56.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.56.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.56.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.56.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.56.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.56.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.56.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.56.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.56.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.56.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.56.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.56.0': + optional: true + + '@sxzz/popperjs-es@2.11.7': {} + + '@trysound/sax@0.2.0': {} + + '@types/estree@1.0.8': {} + + '@types/js-cookie@3.0.6': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.23 + + '@types/lodash@4.17.23': {} + + '@types/node@25.0.10': + dependencies: + undici-types: 7.16.0 + + '@types/svgo@2.6.4': + dependencies: + '@types/node': 25.0.10 + + '@types/web-bluetooth@0.0.20': {} + + '@vitejs/plugin-vue@6.0.3(vite@6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3))(vue@3.5.27(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.53 + vite: 6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3) + vue: 3.5.27(typescript@5.9.3) + + '@volar/language-core@2.4.27': + dependencies: + '@volar/source-map': 2.4.27 + + '@volar/source-map@2.4.27': {} + + '@volar/typescript@2.4.27': + dependencies: + '@volar/language-core': 2.4.27 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.27': + dependencies: + '@babel/parser': 7.28.6 + '@vue/shared': 3.5.27 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.27': + dependencies: + '@vue/compiler-core': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/compiler-sfc@3.5.27': + dependencies: + '@babel/parser': 7.28.6 + '@vue/compiler-core': 3.5.27 + '@vue/compiler-dom': 3.5.27 + '@vue/compiler-ssr': 3.5.27 + '@vue/shared': 3.5.27 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.27': + dependencies: + '@vue/compiler-dom': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/devtools-api@6.6.4': {} + + '@vue/language-core@3.2.4': + dependencies: + '@volar/language-core': 2.4.27 + '@vue/compiler-dom': 3.5.27 + '@vue/shared': 3.5.27 + alien-signals: 3.1.2 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + + '@vue/reactivity@3.5.27': + dependencies: + '@vue/shared': 3.5.27 + + '@vue/runtime-core@3.5.27': + dependencies: + '@vue/reactivity': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/runtime-dom@3.5.27': + dependencies: + '@vue/reactivity': 3.5.27 + '@vue/runtime-core': 3.5.27 + '@vue/shared': 3.5.27 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.27(vue@3.5.27(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.27 + '@vue/shared': 3.5.27 + vue: 3.5.27(typescript@5.9.3) + + '@vue/shared@3.5.27': {} + + '@vueuse/core@10.11.1(vue@3.5.27(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 10.11.1 + '@vueuse/shared': 10.11.1(vue@3.5.27(typescript@5.9.3)) + vue-demi: 0.14.10(vue@3.5.27(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@10.11.1': {} + + '@vueuse/shared@10.11.1(vue@3.5.27(typescript@5.9.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.27(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + acorn@8.15.0: {} + + alien-signals@3.1.2: {} + + ansi-regex@2.1.1: {} + + ansi-styles@2.2.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + arr-diff@4.0.0: {} + + arr-flatten@1.1.0: {} + + arr-union@3.1.0: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-unique@0.3.2: {} + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assign-symbols@1.0.0: {} + + async-function@1.0.0: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + atob@2.1.2: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axios@1.13.4: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + base@0.11.2: + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.1 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + + big.js@5.2.2: {} + + bluebird@3.7.2: {} + + boolbase@1.0.0: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@2.3.2: + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + cache-base@1.0.1: + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.1 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chalk@1.1.3: + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + optional: true + + class-utils@0.3.6: + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + + clone@2.1.2: {} + + collection-visit@1.0.0: + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorjs.io@0.5.2: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@7.2.0: {} + + component-emitter@1.3.1: {} + + confbox@0.1.8: {} + + confbox@0.2.2: {} + + copy-descriptor@0.1.1: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + crypto-js@4.2.0: {} + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + css-what@6.2.2: {} + + csso@4.2.0: + dependencies: + css-tree: 1.1.3 + + csstype@3.2.3: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + dayjs@1.11.19: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-uri-component@0.2.2: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + define-property@0.2.5: + dependencies: + is-descriptor: 0.1.7 + + define-property@1.0.0: + dependencies: + is-descriptor: 1.0.3 + + define-property@2.0.2: + dependencies: + is-descriptor: 1.0.3 + isobject: 3.0.1 + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: + optional: true + + dom-serializer@0.2.2: + dependencies: + domelementtype: 2.3.0 + entities: 2.2.0 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + domelementtype@1.3.1: {} + + domelementtype@2.3.0: {} + + domhandler@2.4.2: + dependencies: + domelementtype: 1.3.1 + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domutils@1.7.0: + dependencies: + dom-serializer: 0.2.2 + domelementtype: 1.3.1 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + element-plus@2.13.1(vue@3.5.27(typescript@5.9.3)): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.27(typescript@5.9.3)) + '@floating-ui/dom': 1.7.4 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.23 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 10.11.1(vue@3.5.27(typescript@5.9.3)) + async-validator: 4.2.5 + dayjs: 1.11.19 + lodash: 4.17.23 + lodash-es: 4.17.23 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.23)(lodash@4.17.23) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.27(typescript@5.9.3) + transitivePeerDependencies: + - '@vue/composition-api' + + emojis-list@3.0.0: {} + + entities@1.1.2: {} + + entities@2.2.0: {} + + entities@7.0.1: {} + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@5.0.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + etag@1.8.1: {} + + expand-brackets@2.1.4: + dependencies: + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.8: {} + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + + extglob@2.0.4: + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@4.0.0: + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + follow-redirects@1.15.11: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + for-in@1.0.2: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fragment-cache@0.2.1: + dependencies: + map-cache: 0.2.2 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-value@2.0.6: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-ansi@2.0.0: + dependencies: + ansi-regex: 2.1.1 + + has-bigints@1.1.0: {} + + has-flag@1.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-value@0.3.1: + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + + has-value@1.0.0: + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + + has-values@0.1.4: {} + + has-values@1.0.0: + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + htmlparser2@3.10.1: + dependencies: + domelementtype: 1.3.1 + domhandler: 2.4.2 + domutils: 1.7.0 + entities: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + + image-size@0.5.5: {} + + immutable@5.1.4: {} + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-accessor-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-buffer@1.1.6: {} + + is-callable@1.2.7: {} + + is-data-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-descriptor@0.1.7: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-descriptor@1.0.3: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@3.0.0: + dependencies: + kind-of: 3.2.2 + + is-number@7.0.0: {} + + is-plain-obj@1.1.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-windows@1.0.2: {} + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isobject@2.1.0: + dependencies: + isarray: 1.0.0 + + isobject@3.0.1: {} + + js-base64@2.6.4: {} + + js-cookie@3.0.5: {} + + js-tokens@9.0.1: {} + + jsencrypt@3.3.2: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + kind-of@4.0.0: + dependencies: + is-buffer: 1.1.6 + + kind-of@5.1.0: {} + + kind-of@6.0.3: {} + + loader-utils@1.4.2: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 1.0.2 + + local-pkg@0.5.1: + dependencies: + mlly: 1.8.0 + pkg-types: 1.3.1 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + + lodash-es@4.17.23: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.23)(lodash@4.17.23): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.23 + lodash-es: 4.17.23 + + lodash@4.17.23: {} + + magic-string@0.26.7: + dependencies: + sourcemap-codec: 1.4.8 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + map-cache@0.2.2: {} + + map-visit@1.0.0: + dependencies: + object-visit: 1.0.1 + + math-intrinsics@1.1.0: {} + + mdn-data@2.0.14: {} + + memoize-one@6.0.0: {} + + merge-options@1.0.1: + dependencies: + is-plain-obj: 1.1.0 + + merge2@1.4.1: {} + + micromatch@3.1.0: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 1.0.0 + extend-shallow: 2.0.1 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 5.1.0 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + mixin-deep@1.3.2: + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.0.0: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + nanomatch@1.2.13: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + node-addon-api@7.1.1: + optional: true + + normalize-wheel-es@1.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + object-copy@0.1.0: + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object-visit@1.0.1: + dependencies: + isobject: 3.0.1 + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.pick@1.3.0: + dependencies: + isobject: 3.0.1 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + pascalcase@0.1.1: {} + + path-browserify@1.0.1: {} + + pathe@0.2.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pinia@2.3.1(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.27(typescript@5.9.3) + vue-demi: 0.14.10(vue@3.5.27(typescript@5.9.3)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@vue/composition-api' + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.8 + pathe: 2.0.3 + + posix-character-classes@0.1.1: {} + + possible-typed-array-names@1.1.0: {} + + postcss-prefix-selector@1.16.1(postcss@5.2.18): + dependencies: + postcss: 5.2.18 + + postcss@5.2.18: + dependencies: + chalk: 1.1.3 + js-base64: 2.6.4 + source-map: 0.5.7 + supports-color: 3.2.3 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + posthtml-parser@0.2.1: + dependencies: + htmlparser2: 3.10.1 + isobject: 2.1.0 + + posthtml-rename-id@1.0.12: + dependencies: + escape-string-regexp: 1.0.5 + + posthtml-render@1.4.0: {} + + posthtml-svg-mode@1.0.3: + dependencies: + merge-options: 1.0.1 + posthtml: 0.9.2 + posthtml-parser: 0.2.1 + posthtml-render: 1.4.0 + + posthtml@0.9.2: + dependencies: + posthtml-parser: 0.2.1 + posthtml-render: 1.4.0 + + proxy-from-env@1.1.0: {} + + quansync@0.2.11: {} + + query-string@4.3.4: + dependencies: + object-assign: 4.1.1 + strict-uri-encode: 1.1.0 + + queue-microtask@1.2.3: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: + optional: true + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regex-not@1.0.2: + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + repeat-element@1.1.4: {} + + repeat-string@1.6.1: {} + + resolve-url@0.2.1: {} + + ret@0.1.15: {} + + reusify@1.1.0: {} + + rollup@4.56.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.56.0 + '@rollup/rollup-android-arm64': 4.56.0 + '@rollup/rollup-darwin-arm64': 4.56.0 + '@rollup/rollup-darwin-x64': 4.56.0 + '@rollup/rollup-freebsd-arm64': 4.56.0 + '@rollup/rollup-freebsd-x64': 4.56.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.56.0 + '@rollup/rollup-linux-arm-musleabihf': 4.56.0 + '@rollup/rollup-linux-arm64-gnu': 4.56.0 + '@rollup/rollup-linux-arm64-musl': 4.56.0 + '@rollup/rollup-linux-loong64-gnu': 4.56.0 + '@rollup/rollup-linux-loong64-musl': 4.56.0 + '@rollup/rollup-linux-ppc64-gnu': 4.56.0 + '@rollup/rollup-linux-ppc64-musl': 4.56.0 + '@rollup/rollup-linux-riscv64-gnu': 4.56.0 + '@rollup/rollup-linux-riscv64-musl': 4.56.0 + '@rollup/rollup-linux-s390x-gnu': 4.56.0 + '@rollup/rollup-linux-x64-gnu': 4.56.0 + '@rollup/rollup-linux-x64-musl': 4.56.0 + '@rollup/rollup-openbsd-x64': 4.56.0 + '@rollup/rollup-openharmony-arm64': 4.56.0 + '@rollup/rollup-win32-arm64-msvc': 4.56.0 + '@rollup/rollup-win32-ia32-msvc': 4.56.0 + '@rollup/rollup-win32-x64-gnu': 4.56.0 + '@rollup/rollup-win32-x64-msvc': 4.56.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-regex@1.1.0: + dependencies: + ret: 0.1.15 + + sass-embedded-all-unknown@1.97.3: + dependencies: + sass: 1.97.3 + optional: true + + sass-embedded-android-arm64@1.97.3: + optional: true + + sass-embedded-android-arm@1.97.3: + optional: true + + sass-embedded-android-riscv64@1.97.3: + optional: true + + sass-embedded-android-x64@1.97.3: + optional: true + + sass-embedded-darwin-arm64@1.97.3: + optional: true + + sass-embedded-darwin-x64@1.97.3: + optional: true + + sass-embedded-linux-arm64@1.97.3: + optional: true + + sass-embedded-linux-arm@1.97.3: + optional: true + + sass-embedded-linux-musl-arm64@1.97.3: + optional: true + + sass-embedded-linux-musl-arm@1.97.3: + optional: true + + sass-embedded-linux-musl-riscv64@1.97.3: + optional: true + + sass-embedded-linux-musl-x64@1.97.3: + optional: true + + sass-embedded-linux-riscv64@1.97.3: + optional: true + + sass-embedded-linux-x64@1.97.3: + optional: true + + sass-embedded-unknown-all@1.97.3: + dependencies: + sass: 1.97.3 + optional: true + + sass-embedded-win32-arm64@1.97.3: + optional: true + + sass-embedded-win32-x64@1.97.3: + optional: true + + sass-embedded@1.97.3: + dependencies: + '@bufbuild/protobuf': 2.11.0 + colorjs.io: 0.5.2 + immutable: 5.1.4 + rxjs: 7.8.2 + supports-color: 8.1.1 + sync-child-process: 1.0.2 + varint: 6.0.0 + optionalDependencies: + sass-embedded-all-unknown: 1.97.3 + sass-embedded-android-arm: 1.97.3 + sass-embedded-android-arm64: 1.97.3 + sass-embedded-android-riscv64: 1.97.3 + sass-embedded-android-x64: 1.97.3 + sass-embedded-darwin-arm64: 1.97.3 + sass-embedded-darwin-x64: 1.97.3 + sass-embedded-linux-arm: 1.97.3 + sass-embedded-linux-arm64: 1.97.3 + sass-embedded-linux-musl-arm: 1.97.3 + sass-embedded-linux-musl-arm64: 1.97.3 + sass-embedded-linux-musl-riscv64: 1.97.3 + sass-embedded-linux-musl-x64: 1.97.3 + sass-embedded-linux-riscv64: 1.97.3 + sass-embedded-linux-x64: 1.97.3 + sass-embedded-unknown-all: 1.97.3 + sass-embedded-win32-arm64: 1.97.3 + sass-embedded-win32-x64: 1.97.3 + + sass@1.97.3: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + optional: true + + scule@1.3.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + snapdragon-node@2.1.1: + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + + snapdragon-util@3.0.1: + dependencies: + kind-of: 3.2.2 + + snapdragon@0.8.2: + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + + source-map-js@1.2.1: {} + + source-map-resolve@0.5.3: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + + source-map-url@0.4.1: {} + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + sourcemap-codec@1.4.8: {} + + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + + stable@0.1.8: {} + + static-extend@0.1.2: + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + strict-uri-encode@1.1.0: {} + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@3.0.1: + dependencies: + ansi-regex: 2.1.1 + + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + + supports-color@2.0.0: {} + + supports-color@3.2.3: + dependencies: + has-flag: 1.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + svg-baker@1.7.0: + dependencies: + bluebird: 3.7.2 + clone: 2.1.2 + he: 1.2.0 + image-size: 0.5.5 + loader-utils: 1.4.2 + merge-options: 1.0.1 + micromatch: 3.1.0 + postcss: 5.2.18 + postcss-prefix-selector: 1.16.1(postcss@5.2.18) + posthtml-rename-id: 1.0.12 + posthtml-svg-mode: 1.0.3 + query-string: 4.3.4 + traverse: 0.6.11 + transitivePeerDependencies: + - supports-color + + svgo@2.8.0: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.1.1 + stable: 0.1.8 + + sync-child-process@1.0.2: + dependencies: + sync-message-port: 1.1.3 + + sync-message-port@1.1.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-object-path@0.3.0: + dependencies: + kind-of: 3.2.2 + + to-regex-range@2.1.1: + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + to-regex@3.0.2: + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + + traverse@0.6.11: + dependencies: + gopd: 1.2.0 + typedarray.prototype.slice: 1.0.5 + which-typed-array: 1.1.20 + + tslib@2.8.1: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typedarray.prototype.slice@1.0.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + math-intrinsics: 1.1.0 + typed-array-buffer: 1.0.3 + typed-array-byte-offset: 1.0.4 + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@7.16.0: {} + + unimport@3.14.6(rollup@4.56.0): + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.56.0) + acorn: 8.15.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.0 + pathe: 2.0.3 + picomatch: 4.0.3 + pkg-types: 1.3.1 + scule: 1.3.0 + strip-literal: 2.1.1 + unplugin: 1.16.1 + transitivePeerDependencies: + - rollup + + union-value@1.0.1: + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + + universalify@2.0.1: {} + + unplugin-auto-import@0.18.6(@vueuse/core@10.11.1(vue@3.5.27(typescript@5.9.3)))(rollup@4.56.0): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.3.0(rollup@4.56.0) + fast-glob: 3.3.3 + local-pkg: 0.5.1 + magic-string: 0.30.21 + minimatch: 9.0.5 + unimport: 3.14.6(rollup@4.56.0) + unplugin: 1.16.1 + optionalDependencies: + '@vueuse/core': 10.11.1(vue@3.5.27(typescript@5.9.3)) + transitivePeerDependencies: + - rollup + + unplugin-vue-setup-extend-plus@1.0.1: + dependencies: + '@vue/compiler-sfc': 3.5.27 + magic-string: 0.26.7 + unplugin: 1.16.1 + + unplugin@1.16.1: + dependencies: + acorn: 8.15.0 + webpack-virtual-modules: 0.6.2 + + unset-value@1.0.0: + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + + urix@0.1.0: {} + + use@3.1.1: {} + + util-deprecate@1.0.2: {} + + varint@6.0.0: {} + + vary@1.1.2: {} + + vite-plugin-compression@0.5.1(vite@6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3)): + dependencies: + chalk: 4.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + vite: 6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3) + transitivePeerDependencies: + - supports-color + + vite-plugin-svg-icons@2.0.1(vite@6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3)): + dependencies: + '@types/svgo': 2.6.4 + cors: 2.8.6 + debug: 4.4.3 + etag: 1.8.1 + fs-extra: 10.1.0 + pathe: 0.2.0 + svg-baker: 1.7.0 + svgo: 2.8.0 + vite: 6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3) + transitivePeerDependencies: + - supports-color + + vite@6.4.1(@types/node@25.0.10)(sass-embedded@1.97.3)(sass@1.97.3): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.56.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.0.10 + fsevents: 2.3.3 + sass: 1.97.3 + sass-embedded: 1.97.3 + + vscode-uri@3.1.0: {} + + vue-demi@0.14.10(vue@3.5.27(typescript@5.9.3)): + dependencies: + vue: 3.5.27(typescript@5.9.3) + + vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.27(typescript@5.9.3) + + vue-tsc@3.2.4(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.27 + '@vue/language-core': 3.2.4 + typescript: 5.9.3 + + vue@3.5.27(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.27 + '@vue/compiler-sfc': 3.5.27 + '@vue/runtime-dom': 3.5.27 + '@vue/server-renderer': 3.5.27(vue@3.5.27(typescript@5.9.3)) + '@vue/shared': 3.5.27 + optionalDependencies: + typescript: 5.9.3 + + web-storage-cache@1.1.1: {} + + webpack-virtual-modules@0.6.2: {} + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 diff --git a/idp/frontend/src/App.vue b/idp/frontend/src/App.vue new file mode 100644 index 0000000..b9d5b98 --- /dev/null +++ b/idp/frontend/src/App.vue @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/idp/frontend/src/api/login.ts b/idp/frontend/src/api/login.ts new file mode 100644 index 0000000..d5b96a6 --- /dev/null +++ b/idp/frontend/src/api/login.ts @@ -0,0 +1,103 @@ +import request from '@/utils/request' +import type { AxiosResponse } from 'axios' + +interface RegisterData { + [key: string]: any +} + +export interface UserInfo { + userId: string | number + userName: string + nickName: string + avatar?: string + roles?: string[] + permissions?: string[] + [key: string]: any +} + +export interface GetInfoResponse { + user: UserInfo + roles: string[] + permissions: string[] + isDefaultModifyPwd?: boolean + isPasswordExpired?: boolean + [key: string]: any +} + +interface LoginResponse { + token?: string + [key: string]: any +} + +interface CaptchaResponse { + captchaEnabled?: boolean + img?: string + uuid?: string +} + +// 登录方法 +export function login(username: string, password: string, code?: string, uuid?: string): Promise> { + const data: string = `username=${username}&password=${password}&code=${code}&uuid=${uuid}` + return request({ + url: '/api/login/account', + headers: { + isToken: false, + repeatSubmit: false, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + method: 'post', + data: data + }) +} +export function authorize(params:string): Promise> { + return request({ + url: '/oauth2/authorize', + headers: { + isToken: true, + repeatSubmit: false, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + method: 'post', + data: params + }) +} + +// 注册方法 +export function register(data: RegisterData): Promise> { + return request({ + url: '/register', + headers: { + isToken: false + }, + method: 'post', + data: data + }) +} + +// 获取用户详细信息 +export function getInfo(): Promise> { + return request({ + url: '/getInfo', + method: 'get' + }) +} + +// 退出方法 +export function logout(): Promise> { + return request({ + url: '/logout', + method: 'post' + }) +} + +// 获取验证码 +export function getCodeImg(): Promise> { + return request({ + url: '/captcha/image', + headers: { + isToken: false + }, + method: 'get', + timeout: 20000 + }) +} \ No newline at end of file diff --git a/idp/frontend/src/assets/401_images/401.gif b/idp/frontend/src/assets/401_images/401.gif new file mode 100644 index 0000000..cd6e0d9 Binary files /dev/null and b/idp/frontend/src/assets/401_images/401.gif differ diff --git a/idp/frontend/src/assets/404_images/404.png b/idp/frontend/src/assets/404_images/404.png new file mode 100644 index 0000000..3d8e230 Binary files /dev/null and b/idp/frontend/src/assets/404_images/404.png differ diff --git a/idp/frontend/src/assets/404_images/404_cloud.png b/idp/frontend/src/assets/404_images/404_cloud.png new file mode 100644 index 0000000..c6281d0 Binary files /dev/null and b/idp/frontend/src/assets/404_images/404_cloud.png differ diff --git a/idp/frontend/src/assets/icons/svg/404.svg b/idp/frontend/src/assets/icons/svg/404.svg new file mode 100644 index 0000000..6df5019 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/bug.svg b/idp/frontend/src/assets/icons/svg/bug.svg new file mode 100644 index 0000000..05a150d --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/bug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/build.svg b/idp/frontend/src/assets/icons/svg/build.svg new file mode 100644 index 0000000..97c4688 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/build.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/button.svg b/idp/frontend/src/assets/icons/svg/button.svg new file mode 100644 index 0000000..904fddc --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/cascader.svg b/idp/frontend/src/assets/icons/svg/cascader.svg new file mode 100644 index 0000000..e256024 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/cascader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/chart.svg b/idp/frontend/src/assets/icons/svg/chart.svg new file mode 100644 index 0000000..27728fb --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/chart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/checkbox.svg b/idp/frontend/src/assets/icons/svg/checkbox.svg new file mode 100644 index 0000000..013fd3a --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/checkbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/clipboard.svg b/idp/frontend/src/assets/icons/svg/clipboard.svg new file mode 100644 index 0000000..90923ff --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/clipboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/code.svg b/idp/frontend/src/assets/icons/svg/code.svg new file mode 100644 index 0000000..5f9c5ab --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/color.svg b/idp/frontend/src/assets/icons/svg/color.svg new file mode 100644 index 0000000..44a81aa --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/component.svg b/idp/frontend/src/assets/icons/svg/component.svg new file mode 100644 index 0000000..29c3458 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/component.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/dashboard.svg b/idp/frontend/src/assets/icons/svg/dashboard.svg new file mode 100644 index 0000000..5317d37 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/date-range.svg b/idp/frontend/src/assets/icons/svg/date-range.svg new file mode 100644 index 0000000..fda571e --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/date-range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/date.svg b/idp/frontend/src/assets/icons/svg/date.svg new file mode 100644 index 0000000..52dc73e --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/date.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/dict.svg b/idp/frontend/src/assets/icons/svg/dict.svg new file mode 100644 index 0000000..4849377 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/dict.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/documentation.svg b/idp/frontend/src/assets/icons/svg/documentation.svg new file mode 100644 index 0000000..7043122 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/documentation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/download.svg b/idp/frontend/src/assets/icons/svg/download.svg new file mode 100644 index 0000000..c896951 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/drag.svg b/idp/frontend/src/assets/icons/svg/drag.svg new file mode 100644 index 0000000..4185d3c --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/druid.svg b/idp/frontend/src/assets/icons/svg/druid.svg new file mode 100644 index 0000000..a2b4b4e --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/druid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/edit.svg b/idp/frontend/src/assets/icons/svg/edit.svg new file mode 100644 index 0000000..d26101f --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/education.svg b/idp/frontend/src/assets/icons/svg/education.svg new file mode 100644 index 0000000..7bfb01d --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/education.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/email.svg b/idp/frontend/src/assets/icons/svg/email.svg new file mode 100644 index 0000000..74d25e2 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/enter.svg b/idp/frontend/src/assets/icons/svg/enter.svg new file mode 100644 index 0000000..f7cabf2 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/enter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/example.svg b/idp/frontend/src/assets/icons/svg/example.svg new file mode 100644 index 0000000..46f42b5 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/excel.svg b/idp/frontend/src/assets/icons/svg/excel.svg new file mode 100644 index 0000000..74d97b8 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/excel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/exit-fullscreen.svg b/idp/frontend/src/assets/icons/svg/exit-fullscreen.svg new file mode 100644 index 0000000..485c128 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/exit-fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/eye-open.svg b/idp/frontend/src/assets/icons/svg/eye-open.svg new file mode 100644 index 0000000..88dcc98 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/eye-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/eye.svg b/idp/frontend/src/assets/icons/svg/eye.svg new file mode 100644 index 0000000..16ed2d8 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/form.svg b/idp/frontend/src/assets/icons/svg/form.svg new file mode 100644 index 0000000..dcbaa18 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/form.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/fullscreen.svg b/idp/frontend/src/assets/icons/svg/fullscreen.svg new file mode 100644 index 0000000..0e86b6f --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/github.svg b/idp/frontend/src/assets/icons/svg/github.svg new file mode 100644 index 0000000..db0a0d4 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/guide.svg b/idp/frontend/src/assets/icons/svg/guide.svg new file mode 100644 index 0000000..b271001 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/guide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/icon.svg b/idp/frontend/src/assets/icons/svg/icon.svg new file mode 100644 index 0000000..82be8ee --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/input.svg b/idp/frontend/src/assets/icons/svg/input.svg new file mode 100644 index 0000000..ab91381 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/input.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/international.svg b/idp/frontend/src/assets/icons/svg/international.svg new file mode 100644 index 0000000..e9b56ee --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/international.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/job.svg b/idp/frontend/src/assets/icons/svg/job.svg new file mode 100644 index 0000000..2a93a25 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/job.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/language.svg b/idp/frontend/src/assets/icons/svg/language.svg new file mode 100644 index 0000000..0082b57 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/link.svg b/idp/frontend/src/assets/icons/svg/link.svg new file mode 100644 index 0000000..48197ba --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/list.svg b/idp/frontend/src/assets/icons/svg/list.svg new file mode 100644 index 0000000..20259ed --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/lock.svg b/idp/frontend/src/assets/icons/svg/lock.svg new file mode 100644 index 0000000..74fee54 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/log.svg b/idp/frontend/src/assets/icons/svg/log.svg new file mode 100644 index 0000000..d879d33 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/log.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/logininfor.svg b/idp/frontend/src/assets/icons/svg/logininfor.svg new file mode 100644 index 0000000..267f844 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/logininfor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/message.svg b/idp/frontend/src/assets/icons/svg/message.svg new file mode 100644 index 0000000..14ca817 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/money.svg b/idp/frontend/src/assets/icons/svg/money.svg new file mode 100644 index 0000000..c1580de --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/money.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/monitor.svg b/idp/frontend/src/assets/icons/svg/monitor.svg new file mode 100644 index 0000000..bc308cb --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/monitor.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/moon.svg b/idp/frontend/src/assets/icons/svg/moon.svg new file mode 100644 index 0000000..ec72d77 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/more-up.svg b/idp/frontend/src/assets/icons/svg/more-up.svg new file mode 100644 index 0000000..d30ac11 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/more-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/nested.svg b/idp/frontend/src/assets/icons/svg/nested.svg new file mode 100644 index 0000000..06713a8 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/nested.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/number.svg b/idp/frontend/src/assets/icons/svg/number.svg new file mode 100644 index 0000000..ad5ce9a --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/number.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/online.svg b/idp/frontend/src/assets/icons/svg/online.svg new file mode 100644 index 0000000..330a202 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/online.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/password.svg b/idp/frontend/src/assets/icons/svg/password.svg new file mode 100644 index 0000000..6c64def --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/pdf.svg b/idp/frontend/src/assets/icons/svg/pdf.svg new file mode 100644 index 0000000..957aa0c --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/pdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/people.svg b/idp/frontend/src/assets/icons/svg/people.svg new file mode 100644 index 0000000..2bd54ae --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/people.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/peoples.svg b/idp/frontend/src/assets/icons/svg/peoples.svg new file mode 100644 index 0000000..aab852e --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/peoples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/phone.svg b/idp/frontend/src/assets/icons/svg/phone.svg new file mode 100644 index 0000000..ab8e8c4 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/post.svg b/idp/frontend/src/assets/icons/svg/post.svg new file mode 100644 index 0000000..2922c61 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/post.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/qq.svg b/idp/frontend/src/assets/icons/svg/qq.svg new file mode 100644 index 0000000..ee13d4e --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/qq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/question.svg b/idp/frontend/src/assets/icons/svg/question.svg new file mode 100644 index 0000000..cf75bd4 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/question.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/radio.svg b/idp/frontend/src/assets/icons/svg/radio.svg new file mode 100644 index 0000000..0cde345 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/radio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/rate.svg b/idp/frontend/src/assets/icons/svg/rate.svg new file mode 100644 index 0000000..aa3b14d --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/rate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/redis-list.svg b/idp/frontend/src/assets/icons/svg/redis-list.svg new file mode 100644 index 0000000..98a15b2 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/redis-list.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/redis.svg b/idp/frontend/src/assets/icons/svg/redis.svg new file mode 100644 index 0000000..2f1d62d --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/redis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/row.svg b/idp/frontend/src/assets/icons/svg/row.svg new file mode 100644 index 0000000..0780992 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/row.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/search.svg b/idp/frontend/src/assets/icons/svg/search.svg new file mode 100644 index 0000000..84233dd --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/select.svg b/idp/frontend/src/assets/icons/svg/select.svg new file mode 100644 index 0000000..d628382 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/server.svg b/idp/frontend/src/assets/icons/svg/server.svg new file mode 100644 index 0000000..eb287e3 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/shopping.svg b/idp/frontend/src/assets/icons/svg/shopping.svg new file mode 100644 index 0000000..87513e7 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/shopping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/size.svg b/idp/frontend/src/assets/icons/svg/size.svg new file mode 100644 index 0000000..1a409f5 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/skill.svg b/idp/frontend/src/assets/icons/svg/skill.svg new file mode 100644 index 0000000..a3b7312 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/skill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/slider.svg b/idp/frontend/src/assets/icons/svg/slider.svg new file mode 100644 index 0000000..fbe4f39 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/slider.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/star.svg b/idp/frontend/src/assets/icons/svg/star.svg new file mode 100644 index 0000000..6cf86e6 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/sunny.svg b/idp/frontend/src/assets/icons/svg/sunny.svg new file mode 100644 index 0000000..cc628bf --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/sunny.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/swagger.svg b/idp/frontend/src/assets/icons/svg/swagger.svg new file mode 100644 index 0000000..05d4e7b --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/swagger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/switch.svg b/idp/frontend/src/assets/icons/svg/switch.svg new file mode 100644 index 0000000..0ba61e3 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/switch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/system.svg b/idp/frontend/src/assets/icons/svg/system.svg new file mode 100644 index 0000000..5992593 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/system.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/tab.svg b/idp/frontend/src/assets/icons/svg/tab.svg new file mode 100644 index 0000000..b4b48e4 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/table.svg b/idp/frontend/src/assets/icons/svg/table.svg new file mode 100644 index 0000000..0e3dc9d --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/textarea.svg b/idp/frontend/src/assets/icons/svg/textarea.svg new file mode 100644 index 0000000..2709f29 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/textarea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/theme.svg b/idp/frontend/src/assets/icons/svg/theme.svg new file mode 100644 index 0000000..5982a2f --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/theme.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/time-range.svg b/idp/frontend/src/assets/icons/svg/time-range.svg new file mode 100644 index 0000000..13c1202 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/time-range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/time.svg b/idp/frontend/src/assets/icons/svg/time.svg new file mode 100644 index 0000000..b376e32 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/time.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/tool.svg b/idp/frontend/src/assets/icons/svg/tool.svg new file mode 100644 index 0000000..48e0e35 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/tool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/tree-table.svg b/idp/frontend/src/assets/icons/svg/tree-table.svg new file mode 100644 index 0000000..8aafdb8 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/tree-table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/tree.svg b/idp/frontend/src/assets/icons/svg/tree.svg new file mode 100644 index 0000000..dd4b7dd --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/upload.svg b/idp/frontend/src/assets/icons/svg/upload.svg new file mode 100644 index 0000000..bae49c0 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/user.svg b/idp/frontend/src/assets/icons/svg/user.svg new file mode 100644 index 0000000..0ba0716 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/validCode.svg b/idp/frontend/src/assets/icons/svg/validCode.svg new file mode 100644 index 0000000..cfb1021 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/validCode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/wechat.svg b/idp/frontend/src/assets/icons/svg/wechat.svg new file mode 100644 index 0000000..c586e55 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/wechat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/icons/svg/zip.svg b/idp/frontend/src/assets/icons/svg/zip.svg new file mode 100644 index 0000000..f806fc4 --- /dev/null +++ b/idp/frontend/src/assets/icons/svg/zip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/idp/frontend/src/assets/images/login.png b/idp/frontend/src/assets/images/login.png new file mode 100644 index 0000000..6b87247 Binary files /dev/null and b/idp/frontend/src/assets/images/login.png differ diff --git a/idp/frontend/src/assets/styles/btn.scss b/idp/frontend/src/assets/styles/btn.scss new file mode 100644 index 0000000..fee3ee1 --- /dev/null +++ b/idp/frontend/src/assets/styles/btn.scss @@ -0,0 +1,99 @@ +@use './variables.module.scss' as *; + +@mixin colorBtn($color) { + background: $color; + + &:hover { + color: $color; + + &:before, + &:after { + background: $color; + } + } +} + +.blue-btn { + @include colorBtn($blue) +} + +.light-blue-btn { + @include colorBtn($light-blue) +} + +.red-btn { + @include colorBtn($red) +} + +.pink-btn { + @include colorBtn($pink) +} + +.green-btn { + @include colorBtn($green) +} + +.tiffany-btn { + @include colorBtn($tiffany) +} + +.yellow-btn { + @include colorBtn($yellow) +} + +.pan-btn { + font-size: 14px; + color: #fff; + padding: 14px 36px; + border-radius: 8px; + border: none; + outline: none; + transition: 600ms ease all; + position: relative; + display: inline-block; + + &:hover { + background: #fff; + + &:before, + &:after { + width: 100%; + transition: 600ms ease all; + } + } + + &:before, + &:after { + content: ''; + position: absolute; + top: 0; + right: 0; + height: 2px; + width: 0; + transition: 400ms ease all; + } + + &::after { + right: inherit; + top: inherit; + left: 0; + bottom: 0; + } +} + +.custom-button { + display: inline-block; + line-height: 1; + white-space: nowrap; + cursor: pointer; + background: #fff; + color: #fff; + -webkit-appearance: none; + text-align: center; + box-sizing: border-box; + outline: 0; + margin: 0; + padding: 10px 15px; + font-size: 14px; + border-radius: 4px; +} diff --git a/idp/frontend/src/assets/styles/element-ui.scss b/idp/frontend/src/assets/styles/element-ui.scss new file mode 100644 index 0000000..0f175f2 --- /dev/null +++ b/idp/frontend/src/assets/styles/element-ui.scss @@ -0,0 +1,96 @@ +// cover some element-ui styles + +.el-breadcrumb__inner, +.el-breadcrumb__inner a { + font-weight: 400 !important; +} + +.el-upload { + input[type="file"] { + display: none !important; + } +} + +.el-upload__input { + display: none; +} + +.cell { + .el-tag { + margin-right: 0px; + } +} + +.small-padding { + .cell { + padding-left: 5px; + padding-right: 5px; + } +} + +.fixed-width { + .el-button--mini { + padding: 7px 10px; + width: 60px; + } +} + +.status-col { + .cell { + padding: 0 10px; + text-align: center; + + .el-tag { + margin-right: 0px; + } + } +} + +// to fixed https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; +} + +// refine element ui upload +.upload-container { + .el-upload { + width: 100%; + + .el-upload-dragger { + width: 100%; + height: 200px; + } + } +} + +// dropdown +.el-dropdown-menu { + a { + display: block + } +} + +// fix date-picker ui bug in filter-item +.el-range-editor.el-input__inner { + display: inline-flex !important; +} + +// to fix el-date-picker css style +.el-range-separator { + box-sizing: content-box; +} + +.el-menu--collapse + > div + > .el-submenu + > .el-submenu__title + .el-submenu__icon-arrow { + display: none; +} + +.el-dropdown .el-dropdown-link{ + color: var(--el-color-primary) !important; +} \ No newline at end of file diff --git a/idp/frontend/src/assets/styles/idp.scss b/idp/frontend/src/assets/styles/idp.scss new file mode 100644 index 0000000..3eb5e0f --- /dev/null +++ b/idp/frontend/src/assets/styles/idp.scss @@ -0,0 +1,308 @@ +/** + * 通用css样式布局处理 + * Copyright (c) 2026 idp + */ + +/** 基础通用 **/ +.pt5 { + padding-top: 5px; +} +.pr5 { + padding-right: 5px; +} +.pb5 { + padding-bottom: 5px; +} +.mt5 { + margin-top: 5px; +} +.mr5 { + margin-right: 5px; +} +.mb5 { + margin-bottom: 5px; +} +.mb8 { + margin-bottom: 8px; +} +.ml5 { + margin-left: 5px; +} +.mt10 { + margin-top: 10px; +} +.mr10 { + margin-right: 10px; +} +.mb10 { + margin-bottom: 10px; +} +.ml10 { + margin-left: 10px; +} +.mt20 { + margin-top: 20px; +} +.mr20 { + margin-right: 20px; +} +.mb20 { + margin-bottom: 20px; +} +.ml20 { + margin-left: 20px; +} + +.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +.el-form--inline { + .el-form-item { + .el-input, .el-cascader, .el-select, .el-autocomplete { + width: 200px; + } + } +} + +.el-form .el-form-item__label { + font-weight: 700; +} +.el-dialog:not(.is-fullscreen) { + margin-top: 6vh !important; +} + +.el-dialog.scrollbar .el-dialog__body { + overflow: auto; + overflow-x: hidden; + max-height: 70vh; + padding: 10px 20px 0; +} + +.el-table { + .el-table__header-wrapper, .el-table__fixed-header-wrapper { + th { + word-break: break-word; + background-color: #f8f8f9 !important; + color: #515a6e; + height: 40px !important; + font-size: 13px; + } + } + .el-table__body-wrapper { + .el-button [class*="el-icon-"] + span { + margin-left: 1px; + } + } +} + +/** 表单布局 **/ +.form-header { + font-size:15px; + color:#6379bb; + border-bottom:1px solid #ddd; + margin:8px 10px 25px 10px; + padding-bottom:5px +} + +/** 表格布局 **/ +.pagination-container { + display: flex; + justify-content: flex-end; + margin-top: 20px; + background-color: transparent !important; +} + +/* 弹窗中的分页器 */ +.el-dialog .pagination-container { + position: static !important; + margin: 10px 0 0 0; + padding: 0 !important; + + .el-pagination { + position: static; + } +} + +/* 移动端适配 */ +@media (max-width: 768px) { + .pagination-container { + .el-pagination { + > .el-pagination__jump { + display: none !important; + } + > .el-pagination__sizes { + display: none !important; + } + } + } +} + +/* tree border */ +.tree-border { + margin-top: 5px; + border: 1px solid var(--el-border-color-light, #e5e6e7); + background: var(--el-bg-color, #FFFFFF) none; + border-radius:4px; + width: 100%; +} + +.el-table .fixed-width .el-button--small { + padding-left: 0; + padding-right: 0; + width: inherit; +} + +/* horizontal el menu */ +.el-menu--horizontal .el-menu-item .svg-icon + span, +.el-menu--horizontal .el-sub-menu__title .svg-icon + span { + margin-left: 3px; +} + +.el-menu--horizontal .el-menu--popup { + min-width: 120px !important; +} + +/** 表格更多操作下拉样式 */ +.el-table .el-dropdown-link { + cursor: pointer; + color: #409EFF; + margin-left: 10px; +} + +.el-table .el-dropdown, .el-icon-arrow-down { + font-size: 12px; +} + +.el-tree-node__content > .el-checkbox { + margin-right: 8px; +} + +.list-group-striped > .list-group-item { + border-left: 0; + border-right: 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; +} + +.list-group { + padding-left: 0px; + list-style: none; +} + +.list-group-item { + border-bottom: 1px solid #e7eaec; + border-top: 1px solid #e7eaec; + margin-bottom: -1px; + padding: 11px 0px; + font-size: 13px; +} + +.pull-right { + float: right !important; +} + +.el-card__header { + padding: 14px 15px 7px !important; + min-height: 40px; +} + +.el-card__body { + padding: 15px 20px 20px 20px !important; +} + +.card-box { + margin-bottom: 10px; +} + +/* button color */ +.el-button--cyan.is-active, +.el-button--cyan:active { + background: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +.el-button--cyan:focus, +.el-button--cyan:hover { + background: #48D1CC; + border-color: #48D1CC; + color: #FFFFFF; +} + +.el-button--cyan { + background-color: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +/* text color */ +.text-navy { + color: #1ab394; +} + +.text-primary { + color: inherit; +} + +.text-success { + color: #1c84c6; +} + +.text-info { + color: #23c6c8; +} + +.text-warning { + color: #f8ac59; +} + +.text-danger { + color: #ed5565; +} + +.text-muted { + color: #888888; +} + +/* image */ +.img-circle { + border-radius: 50%; +} + +.img-lg { + width: 120px; + height: 120px; +} + +.avatar-upload-preview { + position: absolute; + top: 50%; + transform: translate(50%, -50%); + width: 200px; + height: 200px; + border-radius: 50%; + box-shadow: 0 0 4px #ccc; + overflow: hidden; +} + +/* 拖拽列样式 */ +.sortable-ghost{ + opacity: .8; + color: #fff!important; + background: #42b983!important; +} + +/* 表格右侧工具栏样式 */ +.top-right-btn { + margin-left: auto; +} + +/* 分割面板样式 */ +.splitpanes.default-theme .splitpanes__pane { + background-color: var(--splitpanes-default-bg) !important; +} diff --git a/idp/frontend/src/assets/styles/index.scss b/idp/frontend/src/assets/styles/index.scss new file mode 100644 index 0000000..9913e21 --- /dev/null +++ b/idp/frontend/src/assets/styles/index.scss @@ -0,0 +1,179 @@ +//@use './mixin.scss'; +//@use './transition.scss'; +//@use './element-ui.scss'; +//@use './sidebar.scss'; +//@use './btn.scss'; +//@use './idp.scss'; + +body { + height: 100%; + margin: 0; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; +} + +label { + font-weight: 700; +} + +html { + height: 100%; + box-sizing: border-box; +} + +#app { + height: 100%; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +.no-padding { + padding: 0px !important; +} + +.padding-content { + padding: 4px 0; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +div:focus { + outline: none; +} + +.fr { + float: right; +} + +.fl { + float: left; +} + +.pr-5 { + padding-right: 5px; +} + +.pl-5 { + padding-left: 5px; +} + +.block { + display: block; +} + +.pointer { + cursor: pointer; +} + +.inlineBlock { + display: block; +} + +.clearfix { + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} + +aside { + background: #eef1f6; + padding: 8px 24px; + margin-bottom: 20px; + border-radius: 2px; + display: block; + line-height: 32px; + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + color: #2c3e50; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + a { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } + } +} + +//main-container全局样式 +.app-container { + padding: 20px; +} + +.components-container { + margin: 30px 50px; + position: relative; +} + +.text-center { + text-align: center +} + +.sub-navbar { + height: 50px; + line-height: 50px; + position: relative; + width: 100%; + text-align: right; + padding-right: 20px; + transition: 600ms ease position; + background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%); + + .subtitle { + font-size: 20px; + color: #fff; + } + + &.draft { + background: #d0d0d0; + } + + &.deleted { + background: #d0d0d0; + } +} + +.link-type, +.link-type:focus { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } +} + +.filter-container { + padding-bottom: 10px; + + .filter-item { + display: inline-block; + vertical-align: middle; + margin-bottom: 10px; + } +} diff --git a/idp/frontend/src/assets/styles/mixin.scss b/idp/frontend/src/assets/styles/mixin.scss new file mode 100644 index 0000000..06fa061 --- /dev/null +++ b/idp/frontend/src/assets/styles/mixin.scss @@ -0,0 +1,66 @@ +@mixin clearfix { + &:after { + content: ""; + display: table; + clear: both; + } +} + +@mixin scrollBar { + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } +} + +@mixin relative { + position: relative; + width: 100%; + height: 100%; +} + +@mixin pct($pct) { + width: #{$pct}; + position: relative; + margin: 0 auto; +} + +@mixin triangle($width, $height, $color, $direction) { + $width: $width/2; + $color-border-style: $height solid $color; + $transparent-border-style: $width solid transparent; + height: 0; + width: 0; + + @if $direction==up { + border-bottom: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==right { + border-left: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } + + @else if $direction==down { + border-top: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==left { + border-right: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } +} diff --git a/idp/frontend/src/assets/styles/sidebar.scss b/idp/frontend/src/assets/styles/sidebar.scss new file mode 100644 index 0000000..5ee4ac8 --- /dev/null +++ b/idp/frontend/src/assets/styles/sidebar.scss @@ -0,0 +1,238 @@ +@use './variables.module.scss' as vars; + +#app { + + .main-container { + min-height: 100%; + transition: margin-left .28s; + margin-left: vars.$base-sidebar-width; + position: relative; + } + + .sidebarHide { + margin-left: 0!important; + } + + .sidebar-container { + transition: width 0.28s; + width: vars.$base-sidebar-width !important; + height: 100%; + position: fixed; + font-size: 0px; + top: 0; + bottom: 0; + left: 0; + z-index: 1001; + overflow: hidden; + -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + + // reset element-ui css + .horizontal-collapse-transition { + transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; + } + + .scrollbar-wrapper { + overflow-x: hidden !important; + } + + .el-scrollbar__bar.is-vertical { + right: 0px; + } + + .el-scrollbar { + height: 100%; + } + + &.has-logo { + .el-scrollbar { + height: calc(100% - 50px); + } + } + + .is-horizontal { + display: none; + } + + a { + display: inline-block; + width: 100%; + overflow: hidden; + } + + .svg-icon { + margin-right: 10px !important; + } + + .el-menu { + border: none; + height: 100%; + width: 100% !important; + } + + .el-menu-item, .menu-title { + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + } + + .el-menu-item .el-menu-tooltip__trigger { + display: inline-block !important; + } + + // menu hover + .sub-menu-title-noDropdown, + .el-sub-menu__title { + &:hover { + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + & .theme-dark .is-active > .el-sub-menu__title { + color: vars.$base-menu-color-active !important; + } + + & .nest-menu .el-sub-menu>.el-sub-menu__title, + & .el-sub-menu .el-menu-item { + min-width: vars.$base-sidebar-width !important; + + &:hover { + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title, + & .theme-dark .el-sub-menu .el-menu-item { + background-color: vars.$base-sub-menu-background; + + &:hover { + background-color: vars.$base-sub-menu-hover !important; + } + } + } + + .hideSidebar { + .sidebar-container { + width: 54px !important; + } + + .main-container { + margin-left: 54px; + } + + .sub-menu-title-noDropdown { + padding: 0 !important; + position: relative; + + .el-tooltip { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + } + } + + .el-sub-menu { + overflow: hidden; + + &>.el-sub-menu__title { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + + } + } + + .el-menu--collapse { + .el-sub-menu { + &>.el-sub-menu__title { + &>span { + height: 0; + width: 0; + overflow: hidden; + visibility: hidden; + display: inline-block; + } + &>i { + height: 0; + width: 0; + overflow: hidden; + visibility: hidden; + display: inline-block; + } + } + } + } + } + + .el-menu--collapse .el-menu .el-sub-menu { + min-width: vars.$base-sidebar-width !important; + } + + // mobile responsive + .mobile { + .main-container { + margin-left: 0px; + } + + .sidebar-container { + transition: transform .28s; + width: vars.$base-sidebar-width !important; + } + + &.hideSidebar { + .sidebar-container { + pointer-events: none; + transition-duration: 0.3s; + transform: translate3d(-(vars.$base-sidebar-width), 0, 0); + } + } + } + + .withoutAnimation { + + .main-container, + .sidebar-container { + transition: none; + } + } +} + +// when menu collapsed +.el-menu--vertical { + &>.el-menu { + .svg-icon { + margin-right: 16px; + } + } + + .nest-menu .el-sub-menu>.el-sub-menu__title, + .el-menu-item { + &:hover { + // you can use $sub-menuHover + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + // the scroll bar appears when the sub-menu is too long + >.el-menu--popup { + max-height: 100vh; + overflow-y: auto; + + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } + } +} diff --git a/idp/frontend/src/assets/styles/transition.scss b/idp/frontend/src/assets/styles/transition.scss new file mode 100644 index 0000000..1f74a7e --- /dev/null +++ b/idp/frontend/src/assets/styles/transition.scss @@ -0,0 +1,80 @@ +// global transition css + +/* fade */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.28s; +} + +.fade-enter-from, +.fade-leave-active { + opacity: 0; +} + +/* fade-transform */ +.fade-transform--move, +.fade-transform-leave-active, +.fade-transform-enter-active { + transition: all .5s; +} + +.fade-transform-enter-from { + opacity: 0; + transform: translateX(-30px); +} + +.fade-transform-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* breadcrumb transition */ +.breadcrumb-enter-active, +.breadcrumb-leave-active { + transition: all .5s; +} + +.breadcrumb-enter-from, +.breadcrumb-leave-active { + opacity: 0; + transform: translateX(20px); +} + +.breadcrumb-move { + transition: all .5s; +} + +.breadcrumb-leave-active { + position: absolute; +} + +/* 黑暗模式下过渡效果 */ +::view-transition-new(root), ::view-transition-old(root) { + animation: none !important; + backface-visibility: hidden; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.dark::view-transition-old(root) { + z-index: 2147483646; + background: var(--bg-color-dark); +} + +.dark::view-transition-new(root) { + z-index: 1; + background: var(--bg-color); +} + +::view-transition-old(root) { + z-index: 1; + background: var(--bg-color); +} + +::view-transition-new(root) { + z-index: 2147483646; + background: var(--bg-color-dark); +} diff --git a/idp/frontend/src/assets/styles/variables.module.scss b/idp/frontend/src/assets/styles/variables.module.scss new file mode 100644 index 0000000..3a833b5 --- /dev/null +++ b/idp/frontend/src/assets/styles/variables.module.scss @@ -0,0 +1,271 @@ +// base color +$blue: #324157; +$light-blue: #333c46; +$red: #C03639; +$pink: #E65D6E; +$green: #30B08F; +$tiffany: #4AB7BD; +$yellow: #FEC171; +$panGreen: #30B08F; + +// 默认主题变量 +$menuText: #bfcbd9; +$menuActiveText: #409eff; +$menuBg: #304156; +$menuHover: #263445; + +// 浅色主题theme-light +$menuLightBg: #ffffff; +$menuLightHover: #f0f1f5; +$menuLightText: #303133; +$menuLightActiveText: #409EFF; + +// 基础变量 +$base-sidebar-width: 200px; +$sideBarWidth: 200px; + +// 菜单暗色变量 +$base-menu-color: #bfcbd9; +$base-menu-color-active: #f4f4f5; +$base-menu-background: #304156; +$base-sub-menu-background: #1f2d3d; +$base-sub-menu-hover: #001528; + +// 组件变量 +$--color-primary: #409EFF; +$--color-success: #67C23A; +$--color-warning: #E6A23C; +$--color-danger: #F56C6C; +$--color-info: #909399; + +:export { + menuText: $menuText; + menuActiveText: $menuActiveText; + menuBg: $menuBg; + menuHover: $menuHover; + menuLightBg: $menuLightBg; + menuLightHover: $menuLightHover; + menuLightText: $menuLightText; + menuLightActiveText: $menuLightActiveText; + sideBarWidth: $sideBarWidth; + // 导出基础颜色 + blue: $blue; + lightBlue: $light-blue; + red: $red; + pink: $pink; + green: $green; + tiffany: $tiffany; + yellow: $yellow; + panGreen: $panGreen; + // 导出组件颜色 + colorPrimary: $--color-primary; + colorSuccess: $--color-success; + colorWarning: $--color-warning; + colorDanger: $--color-danger; + colorInfo: $--color-info; +} + +// CSS变量定义 +:root { + /* 亮色模式变量 */ + --sidebar-bg: #{$menuBg}; + --sidebar-text: #{$menuText}; + --menu-hover: #{$menuHover}; + + --navbar-bg: #ffffff; + --navbar-text: #303133; + + /* splitpanes default-theme 变量 */ + --splitpanes-default-bg: #ffffff; + +} + +// 暗黑模式变量 +html.dark { + /* 默认通用 */ + --el-bg-color: #141414; + --el-bg-color-overlay: #1d1e1f; + --el-text-color-primary: #ffffff; + --el-text-color-regular: #d0d0d0; + --el-border-color: #434343; + --el-border-color-light: #434343; + + /* primary */ + --primary-bg: #18212b; + + /* 侧边栏 */ + --sidebar-bg: #141414; + --sidebar-text: #ffffff; + --menu-hover: #2d2d2d; + --menu-active-text: #{$menuActiveText}; + + /* 顶部导航栏 */ + --navbar-bg: #141414; + --navbar-text: #ffffff; + --navbar-hover: #141414; + + /* 标签栏 */ + --tags-bg: #141414; + --tags-item-bg: #1d1e1f; + --tags-item-border: #303030; + --tags-item-text: #d0d0d0; + --tags-item-hover: #2d2d2d; + --tags-close-hover: #64666a; + + /* splitpanes 组件暗黑模式变量 */ + --splitpanes-bg: #141414; + --splitpanes-border: #303030; + --splitpanes-splitter-bg: #1d1e1f; + --splitpanes-splitter-hover-bg: #2d2d2d; + + /* blockquote 暗黑模式变量 */ + --blockquote-bg: #1d1e1f; + --blockquote-border: #303030; + --blockquote-text: #d0d0d0; + + /* Cron 时间表达式 模式变量 */ + --cron-border: #303030; + + /* splitpanes default-theme 暗黑模式变量 */ + --splitpanes-default-bg: #141414; + + /* 侧边栏菜单覆盖 */ + .sidebar-container { + .el-menu-item:not(.is-active), .menu-title { + color: var(--el-text-color-regular); + } + & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title, + & .theme-dark .el-sub-menu .el-menu-item { + background-color: var(--el-bg-color) !important; + } + } + + .topmenu-container { + .el-menu-item, + .el-sub-menu .el-sub-menu__title { + color: var(--el-text-color-regular) !important; + } + } + + .topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title{ + color: var(--el-text-color-regular) !important; + } + + /* 顶部栏栏菜单覆盖 */ + .el-menu--horizontal { + .el-menu-item, .el-sub-menu { + &:not(.is-disabled) { + &:hover, + &:focus { + background-color: var(--navbar-hover) !important; + .el-sub-menu__title { + background-color: var(--navbar-hover) !important; + } + } + } + } + } + + /* 分割窗格覆盖 */ + .splitpanes { + background-color: var(--splitpanes-bg); + + .splitpanes__pane { + background-color: var(--splitpanes-bg); + border-color: var(--splitpanes-border); + } + + .splitpanes__splitter { + background-color: var(--splitpanes-splitter-bg); + border-color: var(--splitpanes-border); + + &:hover { + background-color: var(--splitpanes-splitter-hover-bg); + } + + &:before, + &:after { + background-color: var(--splitpanes-border); + } + } + } + + /* 按钮样式覆盖 */ + .el-button--primary.is-plain { + background-color: var(--primary-bg); + border: 1px solid var(--el-color-primary-light-2); + color: var(--el-color-primary-light-2); + + &:hover { + background-color: var(--el-button-hover-bg-color); + border-color: var(--el-button-hover-border-color); + color: var(--el-button-hover-text-color); + } + + &.is-disabled { + background-color: var(--link-active-bg-color); + border-color: var(--el-color-primary-light-3); + color: var(--el-color-primary-light-3); + opacity: 0.5; + } + } + + /* primary tag 样式覆盖 */ + .el-tag--primary { + background-color: var(--primary-bg); + border: 1px solid var(--el-border-color-light); + color: var(--el-color-primary); + } + + /* 表格样式覆盖 */ + .el-table { + --el-table-header-bg-color: var(--el-bg-color-overlay) !important; + --el-table-header-text-color: var(--el-text-color-regular) !important; + --el-table-border-color: var(--el-border-color-light) !important; + --el-table-row-hover-bg-color: var(--el-bg-color-overlay) !important; + + .el-table__header-wrapper, .el-table__fixed-header-wrapper { + th { + background-color: var(--el-bg-color-overlay, #f8f8f9) !important; + color: var(--el-text-color-regular, #515a6e); + } + } + } + + /* 树组件高亮样式覆盖 */ + .el-tree { + .el-tree-node.is-current > .el-tree-node__content { + background-color: var(--el-bg-color-overlay) !important; + color: var(--el-color-primary); + } + + .el-tree-node__content:hover { + background-color: var(--el-bg-color-overlay); + } + } + + /* 下拉菜单样式覆盖 */ + .el-dropdown-menu__item:not(.is-disabled):focus, .el-dropdown-menu__item:not(.is-disabled):hover{ + background-color: var(--navbar-hover) !important; + } + + /* blockquote样式覆盖 */ + blockquote { + background-color: var(--blockquote-bg) !important; + border-left-color: var(--blockquote-border) !important; + color: var(--blockquote-text) !important; + } + + /* 时间表达式标题样式覆盖 */ + .popup-result .title { + background: var(--cron-border); + } + + /* 底部版权样式覆盖 */ + .copyright { + background-color: var(--el-bg-color) !important; + color: var(--el-text-color-regular) !important; + border-top: 1px solid var(--el-bg-color) !important; + } +} + diff --git a/idp/frontend/src/components/SvgIcon/index.ts b/idp/frontend/src/components/SvgIcon/index.ts new file mode 100644 index 0000000..eb858a7 --- /dev/null +++ b/idp/frontend/src/components/SvgIcon/index.ts @@ -0,0 +1,3 @@ +import SvgIcon from './index.vue' + +export default SvgIcon \ No newline at end of file diff --git a/idp/frontend/src/components/SvgIcon/index.vue b/idp/frontend/src/components/SvgIcon/index.vue new file mode 100644 index 0000000..d33c25d --- /dev/null +++ b/idp/frontend/src/components/SvgIcon/index.vue @@ -0,0 +1,46 @@ + + + + + \ No newline at end of file diff --git a/idp/frontend/src/components/SvgIcon/svgicon.ts b/idp/frontend/src/components/SvgIcon/svgicon.ts new file mode 100644 index 0000000..4b1cedd --- /dev/null +++ b/idp/frontend/src/components/SvgIcon/svgicon.ts @@ -0,0 +1,13 @@ +import { App } from 'vue' +import * as components from '@element-plus/icons-vue' + +export default { + install(app: App): void { + Object.keys(components).forEach((key) => { + const componentConfig = components[key as keyof typeof components] + if (componentConfig && typeof componentConfig === 'object' && 'name' in componentConfig) { + app.component(componentConfig.name, componentConfig) + } + }) + } +} \ No newline at end of file diff --git a/idp/frontend/src/main.ts b/idp/frontend/src/main.ts new file mode 100644 index 0000000..7f6241b --- /dev/null +++ b/idp/frontend/src/main.ts @@ -0,0 +1,18 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import App from './App.vue' +import router from './router' +import 'virtual:svg-icons-register' +import SvgIcon from '@/components/SvgIcon' +import elementIcons from '@/components/SvgIcon/svgicon' +const app = createApp(App) + +app.use(createPinia()) +app.use(router) +app.use(ElementPlus) +app.use(elementIcons) +app.component('svg-icon', SvgIcon) + +app.mount('#app') \ No newline at end of file diff --git a/idp/frontend/src/router/index.ts b/idp/frontend/src/router/index.ts new file mode 100644 index 0000000..b39e490 --- /dev/null +++ b/idp/frontend/src/router/index.ts @@ -0,0 +1,87 @@ +import {createRouter, createWebHistory} from 'vue-router' +import {getToken} from "@/utils/auth.ts" +import {logout} from "@/api/login.ts"; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + redirect: '/login' + }, + { + path: '/login', + name: 'login', + component: () => import('../views/LoginPage.vue'), + meta: { + auth: false, + loginTypes: ['password', 'sms', 'dingtalk', 'wecom'] + } + }, + { + path: '/sso', + name: 'sso', + component: () => import('../views/login/Sso.vue'), + meta: { auth: false } + } + ] +}) +const parseURL = ( + url: string | null | undefined +): { basePath: string; paramsObject: { [key: string]: string } } => { + // 如果输入为 null 或 undefined,返回空字符串和空对象 + if (url == null) { + return { basePath: '', paramsObject: {} } + } + + // 找到问号 (?) 的位置,它之前是基础路径,之后是查询参数 + const questionMarkIndex = url.indexOf('?') + let basePath = url + const paramsObject: { [key: string]: string } = {} + + // 如果找到了问号,说明有查询参数 + if (questionMarkIndex !== -1) { + // 获取 basePath + basePath = url.substring(0, questionMarkIndex) + + // 从 URL 中获取查询字符串部分 + const queryString = url.substring(questionMarkIndex + 1) + + // 使用 URLSearchParams 遍历参数 + const searchParams = new URLSearchParams(queryString) + searchParams.forEach((value, key) => { + // 封装进 paramsObject 对象 + paramsObject[key] = value + }) + } + + // 返回 basePath 和 paramsObject + return { basePath, paramsObject } +} +router.beforeEach((to, from, next) => { + if(to.path === '/logout'){ + logout().finally(window.location.href = to.query.redirect || import.meta.env.VITE_APP_DEFAULT_PAGE) + } + + if (getToken()) { + if (to.path === '/sso') { + const redirectPath = from.query.redirect || to.path + // 修复跳转时不带参数的问题 + const redirect = decodeURIComponent(redirectPath as string) + const { paramsObject: query } = parseURL(redirect) + const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect, query } + console.log(nextData) + next() + } else { + next() + } + } else { + if (to.path === '/sso') { + next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页 + } else { + next() + } + } +}) + +export default router \ No newline at end of file diff --git a/idp/frontend/src/settings.ts b/idp/frontend/src/settings.ts new file mode 100644 index 0000000..62d0220 --- /dev/null +++ b/idp/frontend/src/settings.ts @@ -0,0 +1,57 @@ +export default { + /** + * 网页标题 + */ + title: import.meta.env.VITE_APP_TITLE, + + /** + * 侧边栏主题 深色主题theme-dark,浅色主题theme-light + */ + sideTheme: 'theme-dark', + + /** + * 是否系统布局配置 + */ + showSettings: true, + + /** + * 菜单导航模式 1、纯左侧 2、混合(左侧+顶部) 3、纯顶部 + */ + navType: 1, + + /** + * 是否显示 tagsView + */ + tagsView: true, + + /** + * 显示页签图标 + */ + tagsIcon: false, + + /** + * 是否固定头部 + */ + fixedHeader: true, + + /** + * 是否显示logo + */ + sidebarLogo: true, + + /** + * 是否显示动态标题 + */ + dynamicTitle: false, + + /** + * 是否显示底部版权 + */ + footerVisible: false, + + /** + * 底部版权文本内容 + */ + footerContent: 'Copyright © 2026-2032 Lingniu. All Rights Reserved.' +} + diff --git a/idp/frontend/src/stores/auth.ts b/idp/frontend/src/stores/auth.ts new file mode 100644 index 0000000..93caa11 --- /dev/null +++ b/idp/frontend/src/stores/auth.ts @@ -0,0 +1,264 @@ +import { defineStore } from 'pinia' +import type { UserInfo, TokenResponse, LoginRecord, ClientInfo } from '@/types' + +export const useAuthStore = defineStore('auth', { + state: (): { + user: UserInfo | null; + token: TokenResponse | null; + loginHistory: LoginRecord[]; + clientInfo: ClientInfo | null; + isRefreshing: boolean; + refreshTimeout: number | null; + } => ({ + user: null, + token: null, + loginHistory: [], + clientInfo: null, + isRefreshing: false, + refreshTimeout: null + }), + + getters: { + isAuthenticated: (state) => !!state.token?.access_token, + getUsername: (state) => state.user?.username || '', + getNickname: (state) => state.user?.nickname || '', + getAvatar: (state) => state.user?.avatar || '', + getAccessToken: (state) => state.token?.access_token || '', + getRefreshToken: (state) => state.token?.refresh_token || '', + isTokenExpired: (state) => { + if (!state.token?.expires_in) return true; + const expireTime = localStorage.getItem('token_expire_time'); + if (!expireTime) return true; + return Date.now() > parseInt(expireTime); + } + }, + + actions: { + // Set user information + setUser(user: UserInfo) { + this.user = user; + localStorage.setItem('user_info', JSON.stringify(user)); + }, + + // Set token information + setToken(token: TokenResponse) { + this.token = token; + // Calculate and store expire time + const expireTime = Date.now() + (token.expires_in * 1000); + localStorage.setItem('auth_token', JSON.stringify(token)); + localStorage.setItem('token_expire_time', expireTime.toString()); + localStorage.setItem('refresh_token', token.refresh_token); + + // Set up automatic refresh before token expires + this.setupTokenRefresh(); + }, + + // Set client information + setClientInfo(clientInfo: ClientInfo) { + this.clientInfo = clientInfo; + }, + + // Add login record + addLoginRecord(record: LoginRecord) { + this.loginHistory.unshift(record); + // Keep only the last 10 records + if (this.loginHistory.length > 10) { + this.loginHistory.pop(); + } + }, + + // Clear all auth information (logout) + logout() { + this.user = null; + this.token = null; + this.clientInfo = null; + // Clear from localStorage + localStorage.removeItem('auth_token'); + localStorage.removeItem('user_info'); + localStorage.removeItem('token_expire_time'); + localStorage.removeItem('refresh_token'); + // Clear refresh timeout + this.clearTokenRefresh(); + }, + + // Load auth information from localStorage + loadFromStorage() { + const token = localStorage.getItem('auth_token'); + const user = localStorage.getItem('user_info'); + + if (token) { + this.token = JSON.parse(token); + // Set up automatic refresh if token is valid + this.setupTokenRefresh(); + } + + if (user) { + this.user = JSON.parse(user); + } + }, + + // Refresh token + async refreshToken() { + if (this.isRefreshing || !this.getRefreshToken) return; + + try { + this.isRefreshing = true; + + const response = await fetch('http://localhost:8080/oauth2/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${btoa('system-client:secret')}` + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: this.getRefreshToken + }) + }); + + if (!response.ok) { + throw new Error('Failed to refresh token'); + } + + const newToken = await response.json(); + this.setToken(newToken); + + return newToken; + } catch (error) { + console.error('Token refresh failed:', error); + this.logout(); + throw error; + } finally { + this.isRefreshing = false; + } + }, + + // Setup automatic token refresh + setupTokenRefresh() { + // Clear existing timeout + this.clearTokenRefresh(); + + if (!this.token?.expires_in) return; + + const expireTime = localStorage.getItem('token_expire_time'); + if (!expireTime) return; + + // Refresh token 30 seconds before expiration + const refreshTime = parseInt(expireTime) - 30000; + const delay = refreshTime - Date.now(); + + if (delay > 0) { + this.refreshTimeout = window.setTimeout(() => { + this.refreshToken(); + }, delay); + } + }, + + // Clear token refresh timeout + clearTokenRefresh() { + if (this.refreshTimeout) { + clearTimeout(this.refreshTimeout); + this.refreshTimeout = null; + } + }, + + // Get user info from UserInfo endpoint + async fetchUserInfo() { + try { + const response = await fetch('http://localhost:8080/userinfo', { + headers: { + 'Authorization': `Bearer ${this.getAccessToken}` + } + }); + + if (!response.ok) { + throw new Error('Failed to fetch user info'); + } + + const userInfo = await response.json(); + this.setUser(userInfo); + return userInfo; + } catch (error) { + console.error('Failed to fetch user info:', error); + // Try to refresh token and retry + try { + await this.refreshToken(); + const response = await fetch('http://localhost:8080/userinfo', { + headers: { + 'Authorization': `Bearer ${this.getAccessToken}` + } + }); + + if (response.ok) { + const userInfo = await response.json(); + this.setUser(userInfo); + return userInfo; + } + } catch (refreshError) { + console.error('Retry failed after token refresh:', refreshError); + } + throw error; + } + }, + + // Login with username and password + async loginWithPassword(username: string, password: string) { + try { + const response = await fetch('http://localhost:8080/oauth2/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${btoa('system-client:secret')}` + }, + body: new URLSearchParams({ + grant_type: 'password', + username, + password, + scope: 'openid profile read write' + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error_description || 'Login failed'); + } + + const tokenData = await response.json(); + this.setToken(tokenData); + await this.fetchUserInfo(); + + return tokenData; + } catch (error) { + console.error('Password login failed:', error); + throw error; + } + }, + + // Login with SMS code + async loginWithSms(phone: string, code: string) { + try { + const response = await fetch('http://localhost:8080/api/sms/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ phone, code }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'SMS login failed'); + } + + const data = await response.json(); + this.setToken(data.data); + await this.fetchUserInfo(); + + return data.data; + } catch (error) { + console.error('SMS login failed:', error); + throw error; + } + } + } +}) \ No newline at end of file diff --git a/idp/frontend/src/stores/index.ts b/idp/frontend/src/stores/index.ts new file mode 100644 index 0000000..f55eb90 --- /dev/null +++ b/idp/frontend/src/stores/index.ts @@ -0,0 +1,5 @@ +import { createPinia } from 'pinia' + +const store = createPinia() + +export default store \ No newline at end of file diff --git a/idp/frontend/src/stores/user.ts b/idp/frontend/src/stores/user.ts new file mode 100644 index 0000000..d7f897b --- /dev/null +++ b/idp/frontend/src/stores/user.ts @@ -0,0 +1,108 @@ +import router from '@/router' +import { ElMessageBox } from 'element-plus' +import { login, logout, getInfo } from '@/api/login' +import { getToken, removeToken } from '@/utils/auth' +import { isHttp, isEmpty } from "@/utils/validate" +import { defineStore } from 'pinia' +import type { GetInfoResponse } from '@/api/login' + +interface LoginUserInfo { + username: string + password: string + code?: string + uuid?: string +} + +interface UserState { + token: string | null | undefined + id: string | number | null + name: string + nickName: string + avatar: string + roles: string[] + permissions: string[] +} + +const useUserStore = defineStore( + 'user', + { + state: (): UserState => ({ + token: getToken(), + id: '', + name: '', + nickName: '', + avatar: '', + roles: [], + permissions: [] + }), + actions: { + // 登录 + login(userInfo: LoginUserInfo): Promise { + const username = userInfo.username.trim() + const password = userInfo.password + const code = userInfo.code + const uuid = userInfo.uuid + return new Promise((resolve, reject) => { + login(username, password, code, uuid).then(() => { + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // 获取用户信息 + getInfo(): Promise { + return new Promise((resolve, reject) => { + getInfo().then((response) => { + const res = response.data + const user = res.user + let avatar = user.avatar || "" + if (!isHttp(avatar)) { + avatar = (isEmpty(avatar)) ? '' : import.meta.env.VITE_APP_BASE_API + avatar + } + if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 + this.roles = res.roles + this.permissions = res.permissions || [] + } else { + this.roles = ['ROLE_DEFAULT'] + } + this.id = user.userId + this.name = user.userName + this.nickName = user.nickName + this.avatar = avatar + /* 初始密码提示 */ + if(res.isDefaultModifyPwd) { + ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { + router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } }) + }).catch(() => {}) + } + /* 过期密码提示 */ + if(!res.isDefaultModifyPwd && res.isPasswordExpired) { + ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { + router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } }) + }).catch(() => {}) + } + resolve(res) + }).catch(error => { + reject(error) + }) + }) + }, + // 退出系统 + logOut(): Promise { + return new Promise((resolve, reject) => { + logout().then(() => { + this.token = '' + this.roles = [] + this.permissions = [] + removeToken() + resolve() + }).catch(error => { + reject(error) + }) + }) + } + } + }) + +export default useUserStore \ No newline at end of file diff --git a/idp/frontend/src/types/crypto-js.d.ts b/idp/frontend/src/types/crypto-js.d.ts new file mode 100644 index 0000000..03a39ff --- /dev/null +++ b/idp/frontend/src/types/crypto-js.d.ts @@ -0,0 +1,36 @@ +declare module 'crypto-js' { + export const AES: { + encrypt: (data: string, key: string | CryptoJS.lib.WordArray, cfg?: any) => CryptoJS.lib.CipherParams; + decrypt: (encryptedData: string | CryptoJS.lib.CipherParams, key: string | CryptoJS.lib.WordArray, cfg?: any) => CryptoJS.lib.WordArray; + }; + + export const enc: { + Utf8: { + parse: (str: string) => CryptoJS.lib.WordArray; + stringify: (wordArray: CryptoJS.lib.WordArray) => string; + }; + Base64: { + parse: (str: string) => CryptoJS.lib.WordArray; + stringify: (wordArray: CryptoJS.lib.WordArray) => string; + }; + }; + + export const lib: { + WordArray: any; + CipherParams: any; + }; + + export const mode: { + CBC: any; + ECB: any; + }; + + export const pad: { + Pkcs7: any; + ZeroPadding: any; + }; + + export function HmacSHA256(message: string | CryptoJS.lib.WordArray, key: string | CryptoJS.lib.WordArray): CryptoJS.lib.WordArray; + export function SHA256(message: string | CryptoJS.lib.WordArray): CryptoJS.lib.WordArray; + export function MD5(message: string | CryptoJS.lib.WordArray): CryptoJS.lib.WordArray; +} \ No newline at end of file diff --git a/idp/frontend/src/types/index.ts b/idp/frontend/src/types/index.ts new file mode 100644 index 0000000..4effd44 --- /dev/null +++ b/idp/frontend/src/types/index.ts @@ -0,0 +1,55 @@ +// User information interface +export interface UserInfo { + id: number; + uuid: string; + username: string; + nickname: string; + real_name: string; + avatar: string; + email: string; + phone: string; + gender: number; + birthday: string; + source: string; + status: number; +} + +// Token response interface +export interface TokenResponse { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: number; + scope: string; +} + +// Login record interface +export interface LoginRecord { + id: number; + auth_type: string; + username: string; + ip_address: string; + login_time: string; + success: boolean; + fail_reason: string; +} + +// Client information interface +export interface ClientInfo { + client_id: string; + client_name: string; + client_type: string; + redirect_uris: string[]; +} + +// Login form data interface +export interface LoginFormData { + username: string; + password: string; +} + +// SMS login form data interface +export interface SmsLoginFormData { + phone: string; + code: string; +} \ No newline at end of file diff --git a/idp/frontend/src/utils/auth.ts b/idp/frontend/src/utils/auth.ts new file mode 100644 index 0000000..252c885 --- /dev/null +++ b/idp/frontend/src/utils/auth.ts @@ -0,0 +1,15 @@ +import {Storage} from "@/utils/storage.ts"; + +const TokenKey = 'Idp-Token' +const store = new Storage() +export function getToken(): any { + return store.get(TokenKey) +} + +export function setToken(token: string): void { + store.set(TokenKey, token) +} + +export function removeToken(): void { + store.remove(TokenKey) +} \ No newline at end of file diff --git a/idp/frontend/src/utils/cache.ts b/idp/frontend/src/utils/cache.ts new file mode 100644 index 0000000..467b9bf --- /dev/null +++ b/idp/frontend/src/utils/cache.ts @@ -0,0 +1,93 @@ +interface CacheMethods { + set(key: string, value: string): void + get(key: string): string | null + setJSON(key: string, jsonValue: any): void + getJSON(key: string): any + remove(key: string): void +} + +interface CacheExport { + session: CacheMethods + local: CacheMethods +} + +const sessionCache: CacheMethods = { + set(key: string, value: string): void { + if (!sessionStorage) { + return + } + if (key != null && value != null) { + sessionStorage.setItem(key, value) + } + }, + get(key: string): string | null { + if (!sessionStorage) { + return null + } + if (key == null) { + return null + } + return sessionStorage.getItem(key) + }, + setJSON(key: string, jsonValue: any): void { + if (jsonValue != null) { + this.set(key, JSON.stringify(jsonValue)) + } + }, + getJSON(key: string): any { + const value = this.get(key) + if (value != null) { + return JSON.parse(value) + } + return null + }, + remove(key: string): void { + sessionStorage.removeItem(key) + } +} + +const localCache: CacheMethods = { + set(key: string, value: string): void { + if (!localStorage) { + return + } + if (key != null && value != null) { + localStorage.setItem(key, value) + } + }, + get(key: string): string | null { + if (!localStorage) { + return null + } + if (key == null) { + return null + } + return localStorage.getItem(key) + }, + setJSON(key: string, jsonValue: any): void { + if (jsonValue != null) { + this.set(key, JSON.stringify(jsonValue)) + } + }, + getJSON(key: string): any { + const value = this.get(key) + if (value != null) { + return JSON.parse(value) + } + return null + }, + remove(key: string): void { + localStorage.removeItem(key) + } +} + +export default { + /** + * 会话级缓存 + */ + session: sessionCache, + /** + * 本地缓存 + */ + local: localCache +} as CacheExport \ No newline at end of file diff --git a/idp/frontend/src/utils/encryptor.ts b/idp/frontend/src/utils/encryptor.ts new file mode 100644 index 0000000..e53ce2f --- /dev/null +++ b/idp/frontend/src/utils/encryptor.ts @@ -0,0 +1,69 @@ +import CryptoJS from 'crypto-js'; + +// AES encryption key and IV should be loaded from environment variables in production +const AES_KEY = import.meta.env.VITE_AES_KEY || 'default-aes-key-1234567890123456'; +const AES_IV = import.meta.env.VITE_AES_IV || 'default-aes-iv-123456'; + +class SensitiveDataEncryptor { + /** + * Encrypt phone number using AES encryption + * @param phone Phone number to encrypt + * @returns Encrypted phone number + */ + encryptPhone(phone: string): string { + try { + const key = CryptoJS.enc.Utf8.parse(AES_KEY); + const iv = CryptoJS.enc.Utf8.parse(AES_IV); + const encrypted = CryptoJS.AES.encrypt(phone, key, { + iv: iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }); + return encrypted.toString(); + } catch (error) { + console.error('Failed to encrypt phone:', error); + throw new Error('Phone encryption failed'); + } + } + + /** + * Decrypt phone number using AES encryption + * @param encryptedPhone Encrypted phone number + * @returns Decrypted phone number + */ + decryptPhone(encryptedPhone: string): string { + try { + const key = CryptoJS.enc.Utf8.parse(AES_KEY); + const iv = CryptoJS.enc.Utf8.parse(AES_IV); + const decrypted = CryptoJS.AES.decrypt(encryptedPhone, key, { + iv: iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }); + return decrypted.toString(CryptoJS.enc.Utf8); + } catch (error) { + console.error('Failed to decrypt phone:', error); + throw new Error('Phone decryption failed'); + } + } + + /** + * Encrypt general sensitive data + * @param data Data to encrypt + * @returns Encrypted data + */ + encryptData(data: string): string { + return this.encryptPhone(data); + } + + /** + * Decrypt general sensitive data + * @param encryptedData Encrypted data + * @returns Decrypted data + */ + decryptData(encryptedData: string): string { + return this.decryptPhone(encryptedData); + } +} + +export default new SensitiveDataEncryptor(); \ No newline at end of file diff --git a/idp/frontend/src/utils/errorCode.ts b/idp/frontend/src/utils/errorCode.ts new file mode 100644 index 0000000..4ed6286 --- /dev/null +++ b/idp/frontend/src/utils/errorCode.ts @@ -0,0 +1,20 @@ +export interface ErrorCode { + '401': string + '403': string + '404': string + '500': string + '601': string + 'default': string + [key: string]: string +} + +const errorCode: ErrorCode = { + '401': '认证失败,无法访问系统资源', + '403': '当前操作没有权限', + '404': '访问资源不存在', + '500': '服务器内部错误', + '601': '系统异常', + 'default': '系统未知错误,请反馈给管理员' +} + +export default errorCode diff --git a/idp/frontend/src/utils/jsencrypt.ts b/idp/frontend/src/utils/jsencrypt.ts new file mode 100644 index 0000000..d774c1d --- /dev/null +++ b/idp/frontend/src/utils/jsencrypt.ts @@ -0,0 +1,27 @@ +import JSEncrypt from 'jsencrypt' + +const publicKey: string = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' + +const privateKey: string = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' + + '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' + + 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' + + 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' + + 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' + + 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' + + 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' + + 'UP8iWi1Qw0Y=' + +// 加密 +export function encrypt(txt: string): string | false { + const encryptor = new JSEncrypt() + encryptor.setPublicKey(publicKey) // 设置公钥 + return encryptor.encrypt(txt) // 对数据进行加密 +} + +// 解密 +export function decrypt(txt: string): string | false { + const encryptor = new JSEncrypt() + encryptor.setPrivateKey(privateKey) // 设置私钥 + return encryptor.decrypt(txt) // 对数据进行解密 +} \ No newline at end of file diff --git a/idp/frontend/src/utils/request.ts b/idp/frontend/src/utils/request.ts new file mode 100644 index 0000000..67788ec --- /dev/null +++ b/idp/frontend/src/utils/request.ts @@ -0,0 +1,179 @@ +import axios, { AxiosInstance, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios' +import { ElNotification, ElMessageBox, ElMessage } from 'element-plus' +import {getToken, setToken} from '@/utils/auth' +import errorCode, { type ErrorCode } from '@/utils/errorCode' +import cache from '@/utils/cache' +import useUserStore from '@/stores/user' + +// 定义接口类型 +interface CustomRequestConfig { + isToken?: boolean + repeatSubmit?: boolean + interval?: number + [key: string]: any +} + +type RequestConfig = InternalAxiosRequestConfig & { + headers?: InternalAxiosRequestConfig['headers'] & CustomRequestConfig +} + +interface SessionObject { + url: string + data: string + time: number +} + +interface ReloginStatus { + show: boolean +} + + +// 是否显示重新登录 +export let isRelogin: ReloginStatus = { show: false } + +axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' +// 创建axios实例 +const service: AxiosInstance = axios.create({ + // axios中请求配置有baseURL选项,表示请求URL公共部分 + baseURL: import.meta.env.VITE_APP_BASE_API, + // 超时 + timeout: 10000 +}) + +// request拦截器 +service.interceptors.request.use( + (config: RequestConfig): RequestConfig => { + // 是否需要设置 token + const isToken = (config.headers || {}).isToken === true + // 是否需要防止数据重复提交 + const isRepeatSubmit = (config.headers || {}).repeatSubmit === false + // 间隔时间(ms),小于此时间视为重复提交 + const interval = (config.headers || {}).interval || 1000 + if (getToken() && !isToken) { + config.headers = config.headers || {} + config.headers['Idp'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 + } + // get请求映射params参数 + if (config.method === 'get' && config.params) { + let url = config.url + '?' + tansParams(config.params) + url = url.slice(0, -1) + config.params = {} + config.url = url + } + if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { + const requestObj: SessionObject = { + url: config.url || '', + data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data || '', + time: new Date().getTime() + } + const requestSize = Object.keys(JSON.stringify(requestObj)).length // 请求数据大小 + const limitSize = 5 * 1024 * 1024 // 限制存放数据5M + if (requestSize >= limitSize) { + console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。') + return config + } + const sessionObj = cache.session.getJSON('sessionObj') + if (sessionObj === undefined || sessionObj === null || sessionObj === '') { + cache.session.setJSON('sessionObj', requestObj) + } else { + const s_url = sessionObj.url // 请求地址 + const s_data = sessionObj.data // 请求数据 + const s_time = sessionObj.time // 请求时间 + if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { + const message = '数据正在处理,请勿重复提交' + console.warn(`[${s_url}]: ` + message) + throw new Error(message); + } else { + cache.session.setJSON('sessionObj', requestObj) + } + } + } + return config + }, + (error: AxiosError): Promise => { + console.log(error) + return Promise.reject(error) + } +) + +// 响应拦截器 +service.interceptors.response.use( + (res: AxiosResponse): any => { + if(res.headers['idp']){ + setToken(res.headers['idp']) + } + // 未设置状态码则默认成功状态 + const code = res.data.code || 200 + const isInIframe = window.self !== window.top; + // 获取错误信息 + const msg:string = errorCode[code as keyof ErrorCode] || res.data.msg || errorCode['default'] + if (code === 401) { + if (!isRelogin.show && isInIframe) { + isRelogin.show = true + ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { + confirmButtonText: '重新登录', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + isRelogin.show = false + useUserStore().logOut().then(() => { + location.href = '/index' + }) + }).catch(() => { + isRelogin.show = false + }) + } + return Promise.reject('无效的会话,或者会话已过期,请重新登录。') + } else if (code === 500) { + ElMessage({ message: msg, type: 'error' }) + return Promise.reject(new Error(msg)) + } else if (code === 601) { + ElMessage({ message: msg, type: 'warning' }) + return Promise.reject(new Error(msg)) + } else if (code !== 200) { + ElNotification.error({ title: msg }) + return Promise.reject('error') + } else { + return Promise.resolve(res.data) + } + }, + (error: AxiosError): Promise => { + console.log('err' + error) + let { message } = error + if (message === "Network Error") { + message = "后端接口连接异常" + } else if (message.includes("timeout")) { + message = "系统接口请求超时" + } else if (message.includes("Request failed with status code")) { + message = "系统接口" + message.slice(-3) + "异常" + } + ElMessage({ message: message, type: 'error', duration: 5 * 1000 }) + return Promise.reject(error) + } +) +/** + * 参数处理 + * @param params 参数 + */ +export function tansParams(params: any): string { + let result = '' + for (const propName of Object.keys(params)) { + const value = params[propName] + var part = encodeURIComponent(propName) + "=" + if (value !== null && value !== "" && typeof (value) !== "undefined") { + if (typeof value === 'object') { + for (const key of Object.keys(value)) { + if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') { + let params = propName + '[' + key + ']' + var subPart = encodeURIComponent(params) + "=" + result += subPart + encodeURIComponent(value[key]) + "&" + } + } + } else { + result += part + encodeURIComponent(value) + "&" + } + } + } + return result +} +export default service \ No newline at end of file diff --git a/idp/frontend/src/utils/storage.ts b/idp/frontend/src/utils/storage.ts new file mode 100644 index 0000000..083eda0 --- /dev/null +++ b/idp/frontend/src/utils/storage.ts @@ -0,0 +1,358 @@ +/** + * 存储工具类 + * 支持localStorage、sessionStorage和cookie三种存储方式 + */ + +type StorageType = 'localStorage' | 'sessionStorage' | 'cookie'; + +/** + * 存储工具类 + */ +export class Storage { + private storageType: StorageType; + private prefix: string; + + /** + * 构造函数 + * @param storageType 存储类型 + * @param prefix 存储前缀,默认'unified_login_' + */ + constructor(storageType: StorageType = 'localStorage', prefix: string = 'unified_login_') { + this.storageType = storageType; + this.prefix = prefix; + } + + /** + * 设置存储项 + * @param key 存储键 + * @param value 存储值 + * @param options 可选参数,cookie存储时使用 + */ + set(key: string, value: any, options?: { expires?: number; path?: string; domain?: string; secure?: boolean }): void { + const fullKey = this.prefix + key; + const stringValue = typeof value === 'string' ? value : JSON.stringify(value); + + switch (this.storageType) { + case 'localStorage': + this.setLocalStorage(fullKey, stringValue); + break; + case 'sessionStorage': + this.setSessionStorage(fullKey, stringValue); + break; + case 'cookie': + this.setCookie(fullKey, stringValue, options); + break; + } + } + + /** + * 获取存储项 + * @param key 存储键 + * @returns 存储值 + */ + get(key: string): any { + const fullKey = this.prefix + key; + let value: any; + + switch (this.storageType) { + case 'localStorage': + value = this.getLocalStorage(fullKey); + break; + case 'sessionStorage': + value = this.getSessionStorage(fullKey); + break; + case 'cookie': + value = this.getCookie(fullKey); + break; + default: + value = null; + } + + if (value === null) { + return null; + } + + // 尝试解析JSON + try { + return JSON.parse(value); + } catch (e) { + // 如果不是JSON,直接返回字符串 + return value; + } + } + + /** + * 移除存储项 + * @param key 存储键 + */ + remove(key: string): void { + const fullKey = this.prefix + key; + + switch (this.storageType) { + case 'localStorage': + this.removeLocalStorage(fullKey); + break; + case 'sessionStorage': + this.removeSessionStorage(fullKey); + break; + case 'cookie': + this.removeCookie(fullKey); + break; + } + } + + /** + * 清空所有存储项 + */ + clear(): void { + switch (this.storageType) { + case 'localStorage': + this.clearLocalStorage(); + break; + case 'sessionStorage': + this.clearSessionStorage(); + break; + case 'cookie': + this.clearCookie(); + break; + } + } + + /** + * 检查存储类型是否可用 + * @returns boolean 是否可用 + */ + isAvailable(): boolean { + try { + switch (this.storageType) { + case 'localStorage': + return this.isLocalStorageAvailable(); + case 'sessionStorage': + return this.isSessionStorageAvailable(); + case 'cookie': + return typeof document !== 'undefined'; + default: + return false; + } + } catch (e) { + return false; + } + } + + // ------------------------ localStorage 操作 ------------------------ + + /** + * 设置localStorage + */ + private setLocalStorage(key: string, value: string): void { + if (this.isLocalStorageAvailable()) { + localStorage.setItem(key, value); + } + } + + /** + * 获取localStorage + */ + private getLocalStorage(key: string): string | null { + if (this.isLocalStorageAvailable()) { + return localStorage.getItem(key); + } + return null; + } + + /** + * 移除localStorage + */ + private removeLocalStorage(key: string): void { + if (this.isLocalStorageAvailable()) { + localStorage.removeItem(key); + } + } + + /** + * 清空localStorage中所有带前缀的项 + */ + private clearLocalStorage(): void { + if (this.isLocalStorageAvailable()) { + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(this.prefix)) { + localStorage.removeItem(key); + i--; // 索引调整 + } + } + } + } + + /** + * 检查localStorage是否可用 + */ + private isLocalStorageAvailable(): boolean { + if (typeof localStorage === 'undefined') { + return false; + } + try { + const testKey = '__storage_test__'; + localStorage.setItem(testKey, testKey); + localStorage.removeItem(testKey); + return true; + } catch (e) { + return false; + } + } + + // ------------------------ sessionStorage 操作 ------------------------ + + /** + * 设置sessionStorage + */ + private setSessionStorage(key: string, value: string): void { + if (this.isSessionStorageAvailable()) { + sessionStorage.setItem(key, value); + } + } + + /** + * 获取sessionStorage + */ + private getSessionStorage(key: string): string | null { + if (this.isSessionStorageAvailable()) { + return sessionStorage.getItem(key); + } + return null; + } + + /** + * 移除sessionStorage + */ + private removeSessionStorage(key: string): void { + if (this.isSessionStorageAvailable()) { + sessionStorage.removeItem(key); + } + } + + /** + * 清空sessionStorage中所有带前缀的项 + */ + private clearSessionStorage(): void { + if (this.isSessionStorageAvailable()) { + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + if (key && key.startsWith(this.prefix)) { + sessionStorage.removeItem(key); + i--; // 索引调整 + } + } + } + } + + /** + * 检查sessionStorage是否可用 + */ + private isSessionStorageAvailable(): boolean { + if (typeof sessionStorage === 'undefined') { + return false; + } + try { + const testKey = '__storage_test__'; + sessionStorage.setItem(testKey, testKey); + sessionStorage.removeItem(testKey); + return true; + } catch (e) { + return false; + } + } + + // ------------------------ cookie 操作 ------------------------ + + /** + * 设置cookie + */ + private setCookie( + key: string, + value: string, + options?: { expires?: number; path?: string; domain?: string; secure?: boolean } + ): void { + if (typeof document === 'undefined') { + return; + } + + let cookieString = `${key}=${encodeURIComponent(value)}`; + + if (options) { + // 设置过期时间(秒) + if (options.expires) { + const date = new Date(); + date.setTime(date.getTime() + options.expires * 1000); + cookieString += `; expires=${date.toUTCString()}`; + } + + // 设置路径 + if (options.path) { + cookieString += `; path=${options.path}`; + } + + // 设置域名 + if (options.domain) { + cookieString += `; domain=${options.domain}`; + } + + // 设置secure + if (options.secure) { + cookieString += '; secure'; + } + } + + document.cookie = cookieString; + } + + /** + * 获取cookie + */ + private getCookie(key: string): string | null { + if (typeof document === 'undefined') { + return null; + } + + const name = `${key}=`; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + + return null; + } + + /** + * 移除cookie + */ + private removeCookie(key: string): void { + this.setCookie(key, '', { expires: -1 }); + } + + /** + * 清空所有带前缀的cookie + */ + private clearCookie(): void { + if (typeof document === 'undefined') { + return; + } + + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const eqPos = cookie.indexOf('='); + const key = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + if (key.startsWith(this.prefix)) { + this.removeCookie(key); + } + } + } +} diff --git a/idp/frontend/src/utils/validate.ts b/idp/frontend/src/utils/validate.ts new file mode 100644 index 0000000..4910390 --- /dev/null +++ b/idp/frontend/src/utils/validate.ts @@ -0,0 +1,114 @@ +/** + * 路径匹配器 + * @param pattern + * @param path + * @returns {Boolean} + */ +export function isPathMatch(pattern: string, path: string): boolean { + const regexPattern = pattern.replace(/\//g, '\\/').replace(/\*\*/g, '.*').replace(/\*/g, '[^\\/]*') + const regex = new RegExp(`^${regexPattern}$`) + return regex.test(path) +} + +/** + * 判断value字符串是否为空 + * @param value + * @returns {Boolean} + */ +export function isEmpty(value: string | null | undefined): boolean { + if (value == null || value === "" || value === undefined || value === "undefined") { + return true + } + return false +} + +/** + * 判断url是否是http或https + * @param url + * @returns {Boolean} + */ +export function isHttp(url: string): boolean { + return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1 +} + +/** + * 判断path是否为外链 + * @param path + * @returns {Boolean} + */ +export function isExternal(path: string): boolean { + return /^(https?:|mailto:|tel:)/.test(path) +} + +/** + * @param str + * @returns {Boolean} + */ +export function validUsername(str: string): boolean { + const valid_map = ['admin', 'editor'] + return valid_map.indexOf(str.trim()) >= 0 +} + +/** + * @param url + * @returns {Boolean} + */ +export function validURL(url: string): boolean { + const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ + return reg.test(url) +} + +/** + * @param str + * @returns {Boolean} + */ +export function validLowerCase(str: string): boolean { + const reg = /^[a-z]+$/ + return reg.test(str) +} + +/** + * @param str + * @returns {Boolean} + */ +export function validUpperCase(str: string): boolean { + const reg = /^[A-Z]+$/ + return reg.test(str) +} + +/** + * @param str + * @returns {Boolean} + */ +export function validAlphabets(str: string): boolean { + const reg = /^[A-Za-z]+$/ + return reg.test(str) +} + +/** + * @param email + * @returns {Boolean} + */ +export function validEmail(email: string): boolean { + const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + return reg.test(email) +} + +/** + * @param str + * @returns {Boolean} + */ +export function isString(str: any): str is string { + return typeof str === 'string' || str instanceof String +} + +/** + * @param arg + * @returns {Boolean} + */ +export function isArray(arg: any): arg is any[] { + if (typeof Array.isArray === 'undefined') { + return Object.prototype.toString.call(arg) === '[object Array]' + } + return Array.isArray(arg) +} \ No newline at end of file diff --git a/idp/frontend/src/views/LoginPage.vue b/idp/frontend/src/views/LoginPage.vue new file mode 100644 index 0000000..ce967a8 --- /dev/null +++ b/idp/frontend/src/views/LoginPage.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/idp/frontend/src/views/login/PasswordLogin.vue b/idp/frontend/src/views/login/PasswordLogin.vue new file mode 100644 index 0000000..51af161 --- /dev/null +++ b/idp/frontend/src/views/login/PasswordLogin.vue @@ -0,0 +1,126 @@ + + + + + \ No newline at end of file diff --git a/idp/frontend/src/views/login/SmsLogin.vue b/idp/frontend/src/views/login/SmsLogin.vue new file mode 100644 index 0000000..bbd5226 --- /dev/null +++ b/idp/frontend/src/views/login/SmsLogin.vue @@ -0,0 +1,162 @@ + + + + + \ No newline at end of file diff --git a/idp/frontend/src/views/login/Sso.vue b/idp/frontend/src/views/login/Sso.vue new file mode 100644 index 0000000..7be72d6 --- /dev/null +++ b/idp/frontend/src/views/login/Sso.vue @@ -0,0 +1,96 @@ + + + + + \ No newline at end of file diff --git a/idp/frontend/src/views/login/ThirdPartyLogin.vue b/idp/frontend/src/views/login/ThirdPartyLogin.vue new file mode 100644 index 0000000..d11e718 --- /dev/null +++ b/idp/frontend/src/views/login/ThirdPartyLogin.vue @@ -0,0 +1,193 @@ + + + + + \ No newline at end of file diff --git a/idp/frontend/src/vite-env.d.ts b/idp/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..4cc6b76 --- /dev/null +++ b/idp/frontend/src/vite-env.d.ts @@ -0,0 +1,18 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} + +interface ImportMetaEnv { + readonly VITE_APP_TITLE: string + readonly VITE_AES_KEY: string + readonly VITE_AES_IV: string + // 更多环境变量... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} \ No newline at end of file diff --git a/idp/frontend/tsconfig.json b/idp/frontend/tsconfig.json new file mode 100644 index 0000000..7b0b4db --- /dev/null +++ b/idp/frontend/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path mapping */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "~/*": ["./*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","vite/**/*.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/idp/frontend/tsconfig.node.json b/idp/frontend/tsconfig.node.json new file mode 100644 index 0000000..7178009 --- /dev/null +++ b/idp/frontend/tsconfig.node.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "~/*": ["./*"] + } + }, + "include": ["vite.config.ts", "vite/**/*.ts"] +} \ No newline at end of file diff --git a/idp/frontend/vite-env.d.ts b/idp/frontend/vite-env.d.ts new file mode 100644 index 0000000..3947d68 --- /dev/null +++ b/idp/frontend/vite-env.d.ts @@ -0,0 +1,27 @@ +/// + +interface ImportMetaEnv { + readonly VITE_APP_TITLE: string + // 更多环境变量... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} + +// 路径别名类型声明 +declare module '~/*' { + const content: any + export default content +} + +declare module '@/*' { + const content: any + export default content +} diff --git a/idp/frontend/vite.config.ts b/idp/frontend/vite.config.ts new file mode 100644 index 0000000..e8a84e8 --- /dev/null +++ b/idp/frontend/vite.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, loadEnv } from 'vite' +import { resolve } from 'path' +import { fileURLToPath, URL } from 'node:url' +import createVitePlugins from './vite/plugins' + +const baseUrl = 'http://localhost:8000' // 后端接口 + +// https://vitejs.dev/config/ +export default defineConfig(({ mode, command }) => { + const env = loadEnv(mode, process.cwd()) + const { VITE_APP_ENV } = env + return { + // 部署生产环境和开发环境下的URL。 + // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上 + base: VITE_APP_ENV === 'production' ? '/' : '/', + plugins: createVitePlugins(env, command === 'build'), + resolve: { + // https://cn.vitejs.dev/config/#resolve-alias + alias: { + // 设置路径 + '~': fileURLToPath(new URL('.', import.meta.url)), + // 设置别名 + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + // https://cn.vitejs.dev/config/#resolve-extensions + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'] + }, + // 打包配置 + build: { + // https://vite.dev/config/build-options.html + sourcemap: command === 'build' ? false : 'inline', + outDir: 'dist', + assetsDir: 'assets', + chunkSizeWarningLimit: 2000, + rollupOptions: { + output: { + chunkFileNames: 'static/js/[name]-[hash].js', + entryFileNames: 'static/js/[name]-[hash].js', + assetFileNames: 'static/[ext]/[name]-[hash].[ext]' + } + } + }, + // vite 相关配置 + server: { + port: 80, + host: true, + open: true, + proxy: { + // https://cn.vitejs.dev/config/#server-proxy + '/dev-api': { + target: baseUrl, + changeOrigin: true, + rewrite: (p) => p.replace(/^\/dev-api/, '') + }, + // springdoc proxy + '^/v3/api-docs/(.*)': { + target: baseUrl, + changeOrigin: true, + } + } + }, + css: { + postcss: { + plugins: [ + { + postcssPlugin: 'internal:charset-removal', + AtRule: { + charset: (atRule) => { + if (atRule.name === 'charset') { + atRule.remove() + } + } + } + } + ] + } + } + } +}) diff --git a/idp/frontend/vite/plugins/auto-import.ts b/idp/frontend/vite/plugins/auto-import.ts new file mode 100644 index 0000000..bbe992d --- /dev/null +++ b/idp/frontend/vite/plugins/auto-import.ts @@ -0,0 +1,13 @@ +import autoImport from 'unplugin-auto-import/vite' +import type { Plugin } from 'vite' + +export default function createAutoImport(): Plugin { + return autoImport({ + imports: [ + 'vue', + 'vue-router', + 'pinia' + ], + dts: false + }) +} \ No newline at end of file diff --git a/idp/frontend/vite/plugins/compression.ts b/idp/frontend/vite/plugins/compression.ts new file mode 100644 index 0000000..1993040 --- /dev/null +++ b/idp/frontend/vite/plugins/compression.ts @@ -0,0 +1,33 @@ +import compression from 'vite-plugin-compression' +import type { Plugin } from 'vite' + +interface ViteEnv { + VITE_BUILD_COMPRESS?: string + [key: string]: any +} + +export default function createCompression(env: ViteEnv): Plugin[] { + const { VITE_BUILD_COMPRESS } = env + const plugin: Plugin[] = [] + if (VITE_BUILD_COMPRESS) { + const compressList = VITE_BUILD_COMPRESS.split(',') + if (compressList.includes('gzip')) { + plugin.push( + compression({ + ext: '.gz', + deleteOriginFile: false + }) + ) + } + if (compressList.includes('brotli')) { + plugin.push( + compression({ + ext: '.br', + algorithm: 'brotliCompress', + deleteOriginFile: false + }) + ) + } + } + return plugin +} \ No newline at end of file diff --git a/idp/frontend/vite/plugins/index.ts b/idp/frontend/vite/plugins/index.ts new file mode 100644 index 0000000..fc164ea --- /dev/null +++ b/idp/frontend/vite/plugins/index.ts @@ -0,0 +1,20 @@ +import vue from '@vitejs/plugin-vue' +import type { Plugin } from 'vite' + +import createAutoImport from './auto-import' +import createSvgIcon from './svg-icon' +import createCompression from './compression' +import createSetupExtend from './setup-extend' + +interface ViteEnv { + [key: string]: any +} + +export default function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean = false): Plugin[] { + const vitePlugins: Plugin[] = [vue()] + vitePlugins.push(createAutoImport()) + vitePlugins.push(createSetupExtend()) + vitePlugins.push(createSvgIcon(isBuild)) + isBuild && vitePlugins.push(...createCompression(viteEnv)) + return vitePlugins +} \ No newline at end of file diff --git a/idp/frontend/vite/plugins/setup-extend.ts b/idp/frontend/vite/plugins/setup-extend.ts new file mode 100644 index 0000000..2216100 --- /dev/null +++ b/idp/frontend/vite/plugins/setup-extend.ts @@ -0,0 +1,6 @@ +import setupExtend from 'unplugin-vue-setup-extend-plus/vite' +import type { Plugin } from 'vite' + +export default function createSetupExtend(): Plugin { + return setupExtend({}) +} \ No newline at end of file diff --git a/idp/frontend/vite/plugins/svg-icon.ts b/idp/frontend/vite/plugins/svg-icon.ts new file mode 100644 index 0000000..294efe7 --- /dev/null +++ b/idp/frontend/vite/plugins/svg-icon.ts @@ -0,0 +1,11 @@ +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' +import path from 'path' +import type { Plugin } from 'vite' + +export default function createSvgIcon(isBuild: boolean): Plugin { + return createSvgIconsPlugin({ + iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')], + symbolId: 'icon-[dir]-[name]', + svgoOptions: isBuild + }) +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/pom.xml b/sdk/backend/oauth2-login-sdk/pom.xml new file mode 100644 index 0000000..4a8d065 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/pom.xml @@ -0,0 +1,135 @@ + + + 4.0.0 + + oauth2-login-sdk + 1.0-SNAPSHOT + org.lingniu + jar + OAuth2 Login SDK + OAuth2登录SDK后端Java版本 + + 17 + 17 + UTF-8 + 6.5.7 + + + + org.springframework.security + spring-security-config + ${spring-security.version} + + + org.springframework.security + spring-security-oauth2-client + ${spring-security.version} + + + org.springframework.security + spring-security-oauth2-resource-server + ${spring-security.version} + + + org.springframework.security + spring-security-oauth2-jose + ${spring-security.version} + + + org.springframework.boot + spring-boot + 3.5.10 + compile + + + org.springframework.boot + spring-boot-autoconfigure + 3.5.10 + compile + + + org.apache.tomcat.embed + tomcat-embed-core + 11.0.15 + compile + + + org.projectlombok + lombok + 1.18.42 + compile + + + org.springframework.boot + spring-boot-starter-data-redis + 3.5.10 + + + com.fasterxml.jackson.core + jackson-core + 2.20.2 + compile + + + com.fasterxml.jackson.core + jackson-databind + 2.20.2 + compile + + + com.alibaba.fastjson2 + fastjson2 + 2.0.60 + compile + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.19.4 + compile + + + + + + com.nimbusds + nimbus-jose-jwt + 10.0.2 + compile + + + io.netty + netty-codec + 4.1.130.Final + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + MIT License + https://opensource.org/licenses/MIT + repo + + + + \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/common/redis/RedisCache.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/common/redis/RedisCache.java new file mode 100644 index 0000000..4f01c60 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/common/redis/RedisCache.java @@ -0,0 +1,271 @@ +package org.lingniu.sdk.common.redis; + +import org.springframework.data.redis.core.BoundSetOperations; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * spring redis 工具类 + * + * @author portal + **/ +public class RedisCache +{ + public final RedisTemplate redisTemplate; + public RedisCache(RedisTemplate redisTemplate){ + this.redisTemplate = redisTemplate; + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) + { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) + { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) + { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) + { + return redisTemplate.expire(key, timeout, unit); + } + + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) + { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) + { + return redisTemplate.hasKey(key); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) + { + ValueOperations operation = redisTemplate.opsForValue(); + return operation.get(key); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) + { + return redisTemplate.delete(key); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public boolean deleteObject(final Collection collection) + { + return redisTemplate.delete(collection) > 0; + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long setCacheList(final String key, final List dataList) + { + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) + { + return redisTemplate.opsForList().range(key, 0, -1); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public BoundSetOperations setCacheSet(final String key, final Set dataSet) + { + BoundSetOperations setOperation = redisTemplate.boundSetOps(key); + Iterator it = dataSet.iterator(); + while (it.hasNext()) + { + setOperation.add(it.next()); + } + return setOperation; + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) + { + return redisTemplate.opsForSet().members(key); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) + { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) + { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) + { + redisTemplate.opsForHash().put(key, hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) + { + HashOperations opsForHash = redisTemplate.opsForHash(); + return opsForHash.get(key, hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) + { + return redisTemplate.opsForHash().multiGet(key, hKeys); + } + + /** + * 删除Hash中的某条数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return 是否成功 + */ + public boolean deleteCacheMapValue(final String key, final String hKey) + { + return redisTemplate.opsForHash().delete(key, hKey) > 0; + } + public boolean deleteCacheSetValue(final String key, final String hKey) + { + return redisTemplate.opsForSet().remove(key, hKey) > 0; + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) + { + return redisTemplate.keys(pattern); + } + + public void setCacheSet(String key, String value) { + redisTemplate.opsForSet().add(key,value); + } +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/common/serializer/FastJson2JsonRedisSerializer.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/common/serializer/FastJson2JsonRedisSerializer.java new file mode 100644 index 0000000..c6be772 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/common/serializer/FastJson2JsonRedisSerializer.java @@ -0,0 +1,53 @@ +package org.lingniu.sdk.common.serializer; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.Filter; +import org.lingniu.sdk.constant.Constants; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; + +import java.nio.charset.Charset; + +/** + * Redis使用FastJson序列化 + * + * @author portal + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer +{ + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR); + + private Class clazz; + + public FastJson2JsonRedisSerializer(Class clazz) + { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException + { + if (t == null) + { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException + { + if (bytes == null || bytes.length <= 0) + { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + + return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER); + } +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/config/JacksonConfiguration.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/config/JacksonConfiguration.java new file mode 100644 index 0000000..c7fdae3 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/config/JacksonConfiguration.java @@ -0,0 +1,97 @@ +package org.lingniu.sdk.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; + +@Configuration +public class JacksonConfiguration { + + /** + * 默认日期时间格式 + */ + public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + /** + * 默认日期格式 + */ + public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; + + /** + * 默认时区 + */ + public static final String DEFAULT_TIME_ZONE = "Asia/Shanghai"; + + @Bean + @Primary + public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { + ObjectMapper objectMapper = builder.createXmlMapper(false).build(); + + // 配置序列化 + objectMapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + + // 配置反序列化 + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); + + // 配置时区和日期格式 + objectMapper.setTimeZone(TimeZone.getTimeZone(DEFAULT_TIME_ZONE)); + objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT)); + + // 注册JavaTimeModule + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(LocalDateTime.class, + new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))); + javaTimeModule.addDeserializer(LocalDateTime.class, + new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))); + javaTimeModule.addSerializer(LocalDate.class, + new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))); + javaTimeModule.addDeserializer(LocalDate.class, + new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))); + + objectMapper.registerModule(javaTimeModule); + + return objectMapper; + } + + + /** + * 定制器方式(Spring Boot推荐) + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { + return builder -> { + // 序列化配置 + builder.serializationInclusion(JsonInclude.Include.NON_NULL); + builder.failOnEmptyBeans(false); + builder.failOnUnknownProperties(false); + + // 日期格式配置 + builder.timeZone(TimeZone.getTimeZone(DEFAULT_TIME_ZONE)); + builder.simpleDateFormat(DEFAULT_DATE_TIME_FORMAT); + + // 特性配置 + builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + builder.featuresToEnable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); + }; + } +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/config/SdkRedisConfig.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/config/SdkRedisConfig.java new file mode 100644 index 0000000..8081df3 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/config/SdkRedisConfig.java @@ -0,0 +1,68 @@ +package org.lingniu.sdk.config; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import org.lingniu.sdk.common.redis.RedisCache; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module; + +@Configuration("SdkRedisConfig") +public class SdkRedisConfig { + + @Bean + @ConditionalOnMissingBean(name = "sdkRedisTemplate") + @Lazy + public RedisTemplate sdkRedisTemplate( + RedisConnectionFactory connectionFactory, + ObjectMapper objectMapper) { + + // 配置 ObjectMapper 以支持多态类型 + ObjectMapper redisObjectMapper = objectMapper.copy(); + + // 启用默认类型信息,用于反序列化 + redisObjectMapper.activateDefaultTyping( + LaissezFaireSubTypeValidator.instance, + ObjectMapper.DefaultTyping.NON_FINAL, + JsonTypeInfo.As.PROPERTY + ); + + // 注册 Spring Security 和 OAuth2 模块 + redisObjectMapper.registerModules( + SecurityJackson2Modules.getModules(getClass().getClassLoader()) + ); + redisObjectMapper.registerModule(new OAuth2ClientJackson2Module()); + + // 创建 RedisTemplate + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + // 使用 StringRedisSerializer 来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + + // 使用 GenericJackson2JsonRedisSerializer 来序列化和反序列化redis的value值 + GenericJackson2JsonRedisSerializer serializer = + new GenericJackson2JsonRedisSerializer(redisObjectMapper); + + template.setValueSerializer(serializer); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } + @Bean + @ConditionalOnMissingBean(name="redisCache") + public RedisCache redisCache(@Qualifier("redisTemplate")RedisTemplate redisTemplate){ + return new RedisCache(redisTemplate); + } +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/config/SecurityConfig.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/config/SecurityConfig.java new file mode 100644 index 0000000..e8bb411 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/config/SecurityConfig.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020-2023 the original author or 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. + */ +package org.lingniu.sdk.config; + +import org.lingniu.sdk.filter.IdpAuthenticationFilter; +import org.lingniu.sdk.handler.LoginSuccessHandler; +import org.lingniu.sdk.handler.LogoutIdpSuccessHandler; +import org.lingniu.sdk.handler.RedirectHandler; +import org.lingniu.sdk.service.RedisOAuth2AuthorizedClientService; +import org.lingniu.sdk.service.TokenService; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +import static org.springframework.security.config.Customizer.withDefaults; + +/** + * @author Joe Grandja + * @author Dmitriy Dubson + * @author Steve Riesenberg + * @since 0.0.1 + */ +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) +@EnableConfigurationProperties({OAuth2ClientProperties.class}) +@Configuration +public class SecurityConfig { + private final LoginSuccessHandler loginSuccessHandler; + private final OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository; + private final RedisOAuth2AuthorizedClientService redisOAuth2AuthorizedClientService; + private final TokenService tokenService; + private final LogoutIdpSuccessHandler logoutIdpSuccessHandler; + private final RedirectHandler redirectHandler; + + public SecurityConfig(LoginSuccessHandler loginSuccessHandler, OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository, RedisOAuth2AuthorizedClientService redisOAuth2AuthorizedClientService, TokenService tokenService, LogoutIdpSuccessHandler logoutIdpSuccessHandler, RedirectHandler redirectHandler) { + this.loginSuccessHandler = loginSuccessHandler; + this.oAuth2AuthorizedClientRepository = oAuth2AuthorizedClientRepository; + this.redisOAuth2AuthorizedClientService = redisOAuth2AuthorizedClientService; + this.tokenService = tokenService; + this.logoutIdpSuccessHandler = logoutIdpSuccessHandler; + this.redirectHandler = redirectHandler; + } + + // @formatter:off + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtDecoder jwtDecoder) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorize -> + authorize + .anyRequest().authenticated() + ) + .oauth2Login(oauth2-> + oauth2.authorizationEndpoint(authorization ->authorization.authorizationRedirectStrategy(redirectHandler)) + .successHandler(loginSuccessHandler) + .authorizedClientRepository(oAuth2AuthorizedClientRepository) + .authorizedClientService(redisOAuth2AuthorizedClientService) + ) + .exceptionHandling(withDefaults()) + .addFilterBefore(new IdpAuthenticationFilter(tokenService),UsernamePasswordAuthenticationFilter.class) + .oauth2ResourceServer(resource ->resource.jwt(withDefaults())) + .logout(httpSecurityLogoutConfigurer -> + httpSecurityLogoutConfigurer + .logoutSuccessHandler(logoutIdpSuccessHandler) + ); + return http.build(); + } + // @formatter:on + + @Bean + public JwtDecoder jwtDecoder(OAuth2ClientProperties oAuth2ClientProperties) { + RestOperations rest = new RestTemplate(); + + return NimbusJwtDecoder + .withJwkSetUri(oAuth2ClientProperties.getProvider().get("idp").getJwkSetUri()) + .restOperations(rest) + .build(); + } + +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/constant/CacheConstants.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/constant/CacheConstants.java new file mode 100644 index 0000000..a59afef --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/constant/CacheConstants.java @@ -0,0 +1,24 @@ +package org.lingniu.sdk.constant; + +/** + * 缓存的key 常量 + * + * @author portal + */ +public class CacheConstants +{ + // Access Token存储: String结构 + // 格式: access_token:{token} + public static final String ACCESS_TOKEN_KEY = "app_access_token:%s"; + public static final String ACCESS_TOKEN_USER_KEY = "app_access_token_user:%s"; + + // Refresh Token存储: Hash结构 + // 格式: refresh_token:{token} + public static final String REFRESH_TOKEN_KEY = "app_refresh_token:%s"; + + // 用户会话管理 + public static final String USER_SESSIONS = "app_user_sessions:%s"; // userId -> session列表 + + public static final String OAUTH2_CLIENT_KEY_PREFIX = "oauth2:client:"; + +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/constant/Constants.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/constant/Constants.java new file mode 100644 index 0000000..c4181f7 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/constant/Constants.java @@ -0,0 +1,174 @@ +package org.lingniu.sdk.constant; + +import com.nimbusds.openid.connect.sdk.claims.CommonClaimsSet; + +import java.util.Locale; + +/** + * 通用常量信息 + * + * @author portal + */ +public class Constants +{ + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * 系统语言 + */ + public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE; + + /** + * www主域 + */ + public static final String WWW = "www."; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 通用成功标识 + */ + public static final String SUCCESS = "0"; + + /** + * 通用失败标识 + */ + public static final String FAIL = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 所有权限标识 + */ + public static final String ALL_PERMISSION = "*:*:*"; + + /** + * 管理员角色权限标识 + */ + public static final String SUPER_ADMIN = "admin"; + + /** + * 角色权限分隔符 + */ + public static final String ROLE_DELIMITER = ","; + + /** + * 权限标识分隔符 + */ + public static final String PERMISSION_DELIMITER = ","; + + /** + * 验证码有效期(分钟) + */ + public static final Integer CAPTCHA_EXPIRATION = 2; + + /** + * 令牌 + */ + public static final String TOKEN = "token"; + + /** + * 令牌前缀 + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * 令牌前缀 + */ + public static final String LOGIN_USER_KEY = "login_user_key"; + + /** + * 用户ID + */ + public static final String JWT_USERID = "userid"; + + /** + * 用户名称 + */ + public static final String JWT_USERNAME = CommonClaimsSet.SUB_CLAIM_NAME; + + /** + * 用户头像 + */ + public static final String JWT_AVATAR = "avatar"; + + /** + * 创建时间 + */ + public static final String JWT_CREATED = "created"; + + /** + * 用户权限 + */ + public static final String JWT_AUTHORITIES = "authorities"; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全) + */ + public static final String[] JSON_WHITELIST_STR = { "com.portal" }; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = { "com.portal.quartz.task" }; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "org.lingniu.idp.utils.file", "org.lingniu.idp.config", "com.portal.generator" }; +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/constant/UserConstants.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/constant/UserConstants.java new file mode 100644 index 0000000..7bb95d9 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/constant/UserConstants.java @@ -0,0 +1,81 @@ +package org.lingniu.sdk.constant; + +/** + * 用户常量信息 + * + * @author portal + */ +public class UserConstants +{ + /** + * 平台内系统用户的唯一标志 + */ + public static final String SYS_USER = "SYS_USER"; + + /** 正常状态 */ + public static final String NORMAL = "0"; + + /** 异常状态 */ + public static final String EXCEPTION = "1"; + + /** 用户封禁状态 */ + public static final String USER_DISABLE = "1"; + + /** 角色正常状态 */ + public static final String ROLE_NORMAL = "0"; + + /** 角色封禁状态 */ + public static final String ROLE_DISABLE = "1"; + + /** 部门正常状态 */ + public static final String DEPT_NORMAL = "0"; + + /** 部门停用状态 */ + public static final String DEPT_DISABLE = "1"; + + /** 字典正常状态 */ + public static final String DICT_NORMAL = "0"; + + /** 是否为系统默认(是) */ + public static final String YES = "Y"; + + /** 是否菜单外链(是) */ + public static final String YES_FRAME = "0"; + + /** 是否菜单外链(否) */ + public static final String NO_FRAME = "1"; + + /** 菜单类型(目录) */ + public static final String TYPE_DIR = "M"; + + /** 菜单类型(菜单) */ + public static final String TYPE_MENU = "C"; + + /** 菜单类型(按钮) */ + public static final String TYPE_BUTTON = "F"; + + /** Layout组件标识 */ + public final static String LAYOUT = "Layout"; + + /** ParentView组件标识 */ + public final static String PARENT_VIEW = "ParentView"; + + /** InnerLink组件标识 */ + public final static String INNER_LINK = "InnerLink"; + + /** 校验是否唯一的返回标识 */ + public final static boolean UNIQUE = true; + public final static boolean NOT_UNIQUE = false; + + /** + * 用户名长度限制 + */ + public static final int USERNAME_MIN_LENGTH = 2; + public static final int USERNAME_MAX_LENGTH = 20; + + /** + * 密码长度限制 + */ + public static final int PASSWORD_MIN_LENGTH = 5; + public static final int PASSWORD_MAX_LENGTH = 20; +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/filter/IdpAuthenticationFilter.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/filter/IdpAuthenticationFilter.java new file mode 100644 index 0000000..a95eaf5 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/filter/IdpAuthenticationFilter.java @@ -0,0 +1,54 @@ +package org.lingniu.sdk.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.lingniu.sdk.model.token.AccessTokenInfo; +import org.lingniu.sdk.service.TokenService; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class IdpAuthenticationFilter extends OncePerRequestFilter { + private final TokenService tokenService; + + public IdpAuthenticationFilter(TokenService tokenService) { + this.tokenService = tokenService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + try { + AccessTokenInfo accessTokenInfo = null; + // 验证令牌 + if (tokenService.validateAccessToken(request)) { + accessTokenInfo = tokenService.getAccessTokenInfo(request); + }else{ + accessTokenInfo = tokenService.refreshToken(request, response); + } + if(accessTokenInfo!=null){ + // 创建认证对象 + OAuth2AuthenticationToken authentication = + new OAuth2AuthenticationToken( + tokenService.convertPrincipal(accessTokenInfo),null,accessTokenInfo.getClientRegistrationId() + ); + + // 设置认证信息到SecurityContext + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + } catch (Exception e) { + // 令牌验证失败,记录日志但不中断请求 + logger.error("token 验证失败", e); + } + + filterChain.doFilter(request, response); + } + +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/handler/LoginSuccessHandler.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/handler/LoginSuccessHandler.java new file mode 100644 index 0000000..2a49492 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/handler/LoginSuccessHandler.java @@ -0,0 +1,61 @@ +package org.lingniu.sdk.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.lingniu.sdk.model.base.CommonResult; +import org.lingniu.sdk.model.token.TokenInfo; +import org.lingniu.sdk.model.user.UserInfo; +import org.lingniu.sdk.service.TokenService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Map; + +@Component +public class LoginSuccessHandler implements AuthenticationSuccessHandler { + + private final TokenService tokenService; + private final ObjectMapper objectMapper; + + public LoginSuccessHandler(TokenService tokenService, ObjectMapper objectMapper) { + this.tokenService = tokenService; + this.objectMapper = objectMapper; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + OAuth2AuthenticationToken oAuth2AuthenticationToken = (OAuth2AuthenticationToken)authentication; + DefaultOidcUser principal = (DefaultOidcUser)oAuth2AuthenticationToken.getPrincipal(); + Map claims = principal.getUserInfo().getClaims(); + String clientRegistrationId = oAuth2AuthenticationToken.getAuthorizedClientRegistrationId(); + String s = objectMapper.writeValueAsString(claims); + // 生成token + TokenInfo token = tokenService.createToken(principal.getName()); + token.getAccessTokenInfo().setAdditionalInfo(s); + token.getAccessTokenInfo().setClientRegistrationId(clientRegistrationId); + token.getRefreshTokenInfo().setClientRegistrationId(clientRegistrationId); + // 保存token + tokenService.storeTokenInfo(token); + // 将短token放入响应头 + tokenService.setAccessTokenHeader(response,token.getAccessTokenInfo().getTokenValue()); + // 设置Refresh Token到HttpOnly Cookie + tokenService.setRefreshTokenCookie(response, token); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.setStatus(HttpStatus.OK.value()); + objectMapper.writeValue(response.getWriter(), CommonResult.success(CommonResult.success(claims))); + } + + +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/handler/LogoutIdpSuccessHandler.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/handler/LogoutIdpSuccessHandler.java new file mode 100644 index 0000000..4d7c03e --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/handler/LogoutIdpSuccessHandler.java @@ -0,0 +1,36 @@ +package org.lingniu.sdk.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.lingniu.sdk.model.base.CommonResult; +import org.lingniu.sdk.service.TokenService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class LogoutIdpSuccessHandler implements LogoutSuccessHandler{ + private final ObjectMapper objectMapper; + private final TokenService tokenService; + public LogoutIdpSuccessHandler(ObjectMapper objectMapper, TokenService tokenService) { + this.objectMapper = objectMapper; + this.tokenService = tokenService; + } + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + tokenService.clearToken(request); + tokenService.clearRefreshTokenCookie(response); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.setStatus(HttpStatus.OK.value()); + + objectMapper.writeValue(response.getWriter(), CommonResult.success("success")); + } +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/handler/RedirectHandler.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/handler/RedirectHandler.java new file mode 100644 index 0000000..f91d360 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/handler/RedirectHandler.java @@ -0,0 +1,30 @@ +package org.lingniu.sdk.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.lingniu.sdk.model.base.CommonResult; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Map; + +@Component +public class RedirectHandler implements RedirectStrategy { + private final ObjectMapper objectMapper; + + public RedirectHandler(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.setStatus(HttpStatus.OK.value()); + objectMapper.writeValue(response.getWriter(), CommonResult.success(Map.of("redirect_url",url))); + } +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/base/CommonResult.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/base/CommonResult.java new file mode 100644 index 0000000..b6795ca --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/base/CommonResult.java @@ -0,0 +1,78 @@ +package org.lingniu.sdk.model.base; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.springframework.http.HttpStatus; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 错误码 + * + */ + private Integer code; + /** + * 错误提示,用户可阅读 + * + */ + private String msg; + /** + * 返回数据 + */ + private T data; + + /** + * 将传入的 result 对象,转换成另外一个泛型结果的对象 + * + * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 + * + * @param result 传入的 result 对象 + * @param 返回的泛型 + * @return 新的 CommonResult 对象 + */ + public static CommonResult error(CommonResult result) { + return error(result.getCode(), result.getMsg()); + } + + public static CommonResult error(Integer code, String message) { + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + return result; + } + + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.code = HttpStatus.OK.value(); + result.data = data; + result.msg = ""; + return result; + } + + public static boolean isSuccess(Integer code) { + return Objects.equals(code, HttpStatus.OK.value()); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isSuccess() { + return isSuccess(code); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isError() { + return !isSuccess(); + } + +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/token/AccessTokenInfo.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/token/AccessTokenInfo.java new file mode 100644 index 0000000..dc1282e --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/token/AccessTokenInfo.java @@ -0,0 +1,152 @@ +package org.lingniu.sdk.model.token; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +/** + * Access Token 信息 + * 存储在 Redis String 结构中 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AccessTokenInfo { + private String tokenValue; + + /** + * 用户名 + */ + private String username; + + + /** + * 颁发时间 + */ + private Instant issuedAt; + + /** + * 过期时间 + */ + private Instant expiresAt; + + /** + * 关联的刷新Token ID + */ + private String refreshTokenId; + + /** + * JWT ID(如果是JWT token) + */ + private String jti; + + /** + * 附加数据(JSON格式) + */ + private String additionalInfo; + + private String clientRegistrationId; + + /** + * 检查Token是否过期 + */ + public boolean isExpired() { + return expiresAt != null && Instant.now().isAfter(expiresAt); + } + + /** + * 检查Token是否有效 + */ + public boolean isValid() { + return !isExpired(); + } + + /** + * 获取剩余有效时间(秒) + */ + public long getRemainingSeconds() { + if (expiresAt == null) { + return 0; + } + Instant now = Instant.now(); + if (now.isAfter(expiresAt)) { + return 0; + } + return expiresAt.getEpochSecond() - now.getEpochSecond(); + } + + /** + * 获取Token使用时长(秒) + */ + public long getUsedSeconds() { + if (issuedAt == null) { + return 0; + } + Instant end = Instant.now(); + return end.getEpochSecond() - issuedAt.getEpochSecond(); + } + + /** + * 转换为Map,便于Redis存储 + */ + public Map toMap() { + Map map = new HashMap<>(); + map.put("username", username); + map.put("tokenValue",tokenValue); + map.put("issuedAt", issuedAt != null ? issuedAt.toString() : null); + map.put("expiresAt", expiresAt != null ? expiresAt.toString() : null); + map.put("refreshTokenId", refreshTokenId); + map.put("jti", jti); + map.put("clientRegistrationId",clientRegistrationId); + map.put("additionalInfo", additionalInfo); + return map; + } + + /** + * 从Map创建AccessTokenInfo + */ + public static AccessTokenInfo fromMap(Map map) { + if (map == null || map.isEmpty()) { + return null; + } + + AccessTokenInfo.AccessTokenInfoBuilder builder = AccessTokenInfo.builder(); + builder.username((String) map.get("username")); + builder.tokenValue((String) map.get("tokenValue")); + // 处理时间字段 + String issuedAtStr = (String) map.get("issuedAt"); + if (issuedAtStr != null) { + builder.issuedAt(Instant.parse(issuedAtStr)); + } + + String expiresAtStr = (String) map.get("expiresAt"); + if (expiresAtStr != null) { + builder.expiresAt(Instant.parse(expiresAtStr)); + } + + builder.refreshTokenId((String) map.get("refreshTokenId")); + builder.jti((String) map.get("jti")); + builder.clientRegistrationId((String) map.get("clientRegistrationId")); + builder.additionalInfo((String) map.get("additionalInfo")); + + return builder.build(); + } + + /** + * 简化的用户信息(用于接口返回) + */ + public Map toSimpleInfo() { + Map info = new HashMap<>(); + info.put("username", username); + info.put("expiresAt", expiresAt != null ? expiresAt.toEpochMilli() : null); + info.put("issuedAt", issuedAt != null ? issuedAt.toEpochMilli() : null); + info.put("valid", isValid()); + return info; + } +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/token/RefreshTokenInfo.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/token/RefreshTokenInfo.java new file mode 100644 index 0000000..1346e6b --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/token/RefreshTokenInfo.java @@ -0,0 +1,226 @@ +package org.lingniu.sdk.model.token; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +/** + * Refresh Token 信息 + * 存储在 Redis Hash 结构中 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RefreshTokenInfo { + private String tokenValue; + + /** + * 用户名 + */ + private String username; + + + /** + * 创建时间 + */ + private Instant createdAt; + + /** + * 最后使用时间 + */ + private Instant lastUsedAt; + + /** + * 过期时间 + */ + private Instant expiresAt; + + /** + * 对应accessToken + */ + private String accessToken; + /** + * 关联的Access Token数量(用于统计) + */ + private int accessTokenCount; + + /** + * 使用次数 + */ + private int usageCount; + + + + /** + * 附加数据(JSON格式) + */ + private String additionalInfo; + + private String clientRegistrationId; + + + /** + * 检查Refresh Token是否过期 + */ + public boolean isExpired() { + return expiresAt != null && Instant.now().isAfter(expiresAt); + } + + /** + * 检查Refresh Token是否有效 + */ + public boolean isValid() { + return !isExpired(); + } + + /** + * 获取剩余有效时间(秒) + */ + public long getRemainingSeconds() { + if (expiresAt == null) { + return 0; + } + Instant now = Instant.now(); + if (now.isAfter(expiresAt)) { + return 0; + } + return expiresAt.getEpochSecond() - now.getEpochSecond(); + } + + /** + * 获取活跃天数(创建到现在) + */ + public long getActiveDays() { + if (createdAt == null) { + return 0; + } + Instant end = Instant.now(); + long seconds = end.getEpochSecond() - createdAt.getEpochSecond(); + return seconds / (24 * 3600); + } + + /** + * 获取闲置天数(最后使用到现在) + */ + public long getIdleDays() { + if (lastUsedAt == null) { + return getActiveDays(); + } + Instant now = Instant.now(); + long seconds = now.getEpochSecond() - lastUsedAt.getEpochSecond(); + return seconds / (24 * 3600); + } + + /** + * 增加使用计数 + */ + public void incrementUsage() { + this.usageCount++; + this.lastUsedAt = Instant.now(); + } + + public void incrementAccessTokenUsage(String accessToken) { + this.accessTokenCount++; + this.accessToken = accessToken; + } + + + /** + * 转换为Map,便于Redis存储 + */ + public Map toMap() { + Map hash = new HashMap<>(); + hash.put("username", username != null ? username : ""); + hash.put("tokenValue", tokenValue != null ? tokenValue : ""); + hash.put("accessToken", accessToken != null ? accessToken : ""); + hash.put("createdAt", createdAt != null ? createdAt.toString() : ""); + hash.put("lastUsedAt", lastUsedAt != null ? lastUsedAt.toString() : ""); + hash.put("expiresAt", expiresAt != null ? expiresAt.toString() : ""); + hash.put("accessTokenCount", Integer.toString(accessTokenCount)); + hash.put("usageCount", Integer.toString(usageCount)); + hash.put("clientRegistrationId", clientRegistrationId); + hash.put("additionalInfo", additionalInfo != null ? additionalInfo : ""); + + return hash; + } + public Map toUpdateMap() { + Map hash = new HashMap<>(); + hash.put("lastUsedAt", lastUsedAt != null ? lastUsedAt.toString() : ""); + hash.put("accessTokenCount", Integer.toString(accessTokenCount)); + hash.put("accessToken", accessToken != null ? accessToken : ""); + hash.put("usageCount", Integer.toString(usageCount)); + return hash; + } + + /** + * 从Redis Hash创建RefreshTokenInfo + */ + public static RefreshTokenInfo fromMap(Map hash) { + if (hash == null || hash.isEmpty()) { + return null; + } + + RefreshTokenInfoBuilder builder = RefreshTokenInfo.builder(); + builder.username((String) hash.getOrDefault("username", "")); + builder.accessToken((String) hash.getOrDefault("accessToken", "")); + builder.tokenValue((String) hash.getOrDefault("tokenValue", "")); + builder.clientRegistrationId((String) hash.getOrDefault("clientRegistrationId", "")); + + // 处理时间字段 + String createdAtStr = (String)hash.get("createdAt"); + if (createdAtStr != null && !createdAtStr.isEmpty()) { + try { + builder.createdAt(Instant.parse(createdAtStr)); + } catch (Exception e) { + // 解析失败,使用当前时间 + builder.createdAt(Instant.now()); + } + } + + String lastUsedAtStr = (String)hash.get("lastUsedAt"); + if (lastUsedAtStr != null && !lastUsedAtStr.isEmpty()) { + try { + builder.lastUsedAt(Instant.parse(lastUsedAtStr)); + } catch (Exception e) { + // 解析失败,忽略 + } + } + + String expiresAtStr = (String)hash.get("expiresAt"); + if (expiresAtStr != null && !expiresAtStr.isEmpty()) { + try { + builder.expiresAt(Instant.parse(expiresAtStr)); + } catch (Exception e) { + // 解析失败,忽略 + } + } + + // 处理数值字段 + String accessTokenCountStr = (String)hash.get("accessTokenCount"); + if (accessTokenCountStr != null && !accessTokenCountStr.isEmpty()) { + try { + builder.accessTokenCount(Integer.parseInt(accessTokenCountStr)); + } catch (NumberFormatException e) { + builder.accessTokenCount(0); + } + } + + String usageCountStr = (String)hash.get("usageCount"); + if (usageCountStr != null && !usageCountStr.isEmpty()) { + try { + builder.usageCount(Integer.parseInt(usageCountStr)); + } catch (NumberFormatException e) { + builder.usageCount(0); + } + } + + return builder.build(); + } +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/token/TokenInfo.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/token/TokenInfo.java new file mode 100644 index 0000000..f759f5a --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/token/TokenInfo.java @@ -0,0 +1,22 @@ +package org.lingniu.sdk.model.token; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; + +// TokenInfo.java +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TokenInfo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + private AccessTokenInfo accessTokenInfo; + private RefreshTokenInfo refreshTokenInfo; +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/DataPermission.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/DataPermission.java new file mode 100644 index 0000000..c782163 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/DataPermission.java @@ -0,0 +1,23 @@ +package org.lingniu.sdk.model.user; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@NoArgsConstructor +public class DataPermission { + /** 允许全部*/ + private boolean allowAll; + /**仅自己*/ + private boolean onlySelf; + /**部门列表*/ + private Set deptList; + /**地区*/ + private Set areas; + +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/UserDept.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/UserDept.java new file mode 100644 index 0000000..91fabd2 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/UserDept.java @@ -0,0 +1,36 @@ +package org.lingniu.sdk.model.user; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@NoArgsConstructor +public class UserDept implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** 部门ID */ + private Long deptId; + + /** 父部门ID */ + private Long parentId; + + /** 祖级列表 */ + private String ancestors; + + /** 部门名称 */ + private String deptName; + + /** 显示顺序 */ + private Integer orderNum; + + /** 负责人 */ + private String leader; + /** 部门状态:0正常,1停用 */ + private String status; +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/UserInfo.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/UserInfo.java new file mode 100644 index 0000000..c652072 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/UserInfo.java @@ -0,0 +1,78 @@ +package org.lingniu.sdk.model.user; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@NoArgsConstructor +public class UserInfo implements OAuth2User, Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * userid + */ + private Long userId; + /** + * 用户账号 + */ + private String username; + /** + * 用户名 + */ + private String nickName; + /** + * 性别 + */ + private String sex; + /** + * 当前部门 + */ + private Long currentDeptId; + /** + * 用户部门列表 + */ + private List userDepts; + /** + * 用户岗位列表 + */ + private List userPosts; + /** + * 用户数据权限 + */ + private DataPermission dataPermission; + /** + * 权限列表 + */ + private Set permissions; + /** + * 角色列表 + */ + private Set roles; + + + @Override + public String getName() { + return username; + } + + @Override + public Map getAttributes() { + return Map.of(); + } + + @Override + public Collection getAuthorities() { + return List.of(); + } +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/UserPost.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/UserPost.java new file mode 100644 index 0000000..7b78dd2 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/model/user/UserPost.java @@ -0,0 +1,31 @@ +package org.lingniu.sdk.model.user; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@NoArgsConstructor +public class UserPost implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** 岗位序号 */ + private Long postId; + + /** 岗位编码 */ + private String postCode; + + /** 岗位名称 */ + private String postName; + + /** 岗位排序 */ + private Integer postSort; + + /** 状态(0正常 1停用) */ + private String status; +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisAccessTokenService.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisAccessTokenService.java new file mode 100644 index 0000000..fa3a1fc --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisAccessTokenService.java @@ -0,0 +1,89 @@ +package org.lingniu.sdk.service; + +import lombok.extern.slf4j.Slf4j; +import org.lingniu.sdk.common.redis.RedisCache; +import org.lingniu.sdk.constant.CacheConstants; +import org.lingniu.sdk.model.token.AccessTokenInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Map; + +@Component +@Slf4j +public class RedisAccessTokenService { + + @Autowired + private RedisCache redisCache; + + private final long ACCESS_TOKEN_EXPIRE = 3600; // 1小时 + + + /** + * 存储Access Token到Redis + */ + public void storeAccessToken(AccessTokenInfo tokenInfo) { + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, tokenInfo.getTokenValue()); + try { + redisCache.setCacheMap(key,tokenInfo.toMap()); + Instant expiresAt = tokenInfo.getExpiresAt(); + long expire = ACCESS_TOKEN_EXPIRE; + if(expiresAt!=null){ + expire = expiresAt.getEpochSecond() - Instant.now().getEpochSecond(); + } + + redisCache.expire(key,expire); + } catch (Exception e) { + log.error("存储Access Token失败", e); + } + } + + /** + * 验证Access Token + */ + public boolean validateAccessToken(String token) { + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, token); + if(!redisCache.hasKey(key)){ + return false; + } + AccessTokenInfo accessTokenInfo = getAccessTokenInfo(token); + if(accessTokenInfo==null){ + return false; + } + return accessTokenInfo.isValid(); + } + + + /** + * 删除Access Token + */ + public boolean removeAccessToken(String token) { + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, token); + return redisCache.deleteObject(key); + } + + /** + * 获取Access Token信息 + */ + public AccessTokenInfo getAccessTokenInfo(String token) { + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, token); + Map cacheMap = redisCache.getCacheMap(key); + if(cacheMap!=null){ + return AccessTokenInfo.fromMap(cacheMap); + } + return null; + } + + /** + * 作废 删除 + * @param tokenInfo + */ + public void revokeAccessToken(AccessTokenInfo tokenInfo) { + if(tokenInfo==null){ + return; + } + String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, tokenInfo.getTokenValue()); + redisCache.deleteObject(key); + } +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientRepository.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientRepository.java new file mode 100644 index 0000000..a1c1131 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientRepository.java @@ -0,0 +1,248 @@ +package org.lingniu.sdk.service; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.lingniu.sdk.constant.CacheConstants; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Component +public class RedisOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository { + + private final RedisTemplate redisTemplate; + private final ClientRegistrationRepository clientRegistrationRepository; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public RedisOAuth2AuthorizedClientRepository( + RedisTemplate redisTemplate, + ClientRegistrationRepository clientRegistrationRepository) { + this.redisTemplate = redisTemplate; + this.clientRegistrationRepository = clientRegistrationRepository; + } + + @Override + public T loadAuthorizedClient( + String clientRegistrationId, + Authentication principal, + HttpServletRequest request) { + + if (principal == null || !principal.isAuthenticated()) { + return null; + } + + String principalName = principal.getName(); + + String key = buildClientKey(principalName, clientRegistrationId); + + try { + Object data = redisTemplate.opsForValue().get(key); + if (data == null) { + return null; + } + + // 反序列化 + Map clientData = objectMapper.convertValue(data, new TypeReference>() {}); + + // 重建 ClientRegistration + ClientRegistration clientRegistration = clientRegistrationRepository + .findByRegistrationId(clientRegistrationId); + + if (clientRegistration == null) { + return null; + } + + // 重建 OAuth2AccessToken + OAuth2AccessToken accessToken = rebuildAccessToken( + (Map) clientData.get("accessToken") + ); + + // 重建 OAuth2RefreshToken(如果有) + OAuth2RefreshToken refreshToken = null; + if (clientData.containsKey("refreshToken")) { + refreshToken = rebuildRefreshToken( + (Map) clientData.get("refreshToken") + ); + } + + // 重建 OAuth2AuthorizedClient + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + clientRegistration, + principalName, + accessToken, + refreshToken + ); + + + @SuppressWarnings("unchecked") + T result = (T) authorizedClient; + return result; + + } catch (Exception e) { + // 如果反序列化失败,删除损坏的数据 + redisTemplate.delete(key); + return null; + } + } + + @Override + public void saveAuthorizedClient( + OAuth2AuthorizedClient authorizedClient, + Authentication principal, + HttpServletRequest request, + HttpServletResponse response) { + + if (principal == null || !principal.isAuthenticated()) { + return; + } + + String principalName = principal.getName(); + String clientRegistrationId = authorizedClient.getClientRegistration().getRegistrationId(); + String key = buildClientKey(principalName, clientRegistrationId); + try { + // 序列化 OAuth2AuthorizedClient + Map clientData = new HashMap<>(); + + // 存储 ClientRegistrationId + clientData.put("clientRegistrationId", clientRegistrationId); + clientData.put("principalName", principalName); + + // 序列化 AccessToken + OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); + Map accessTokenData = new HashMap<>(); + accessTokenData.put("tokenValue", accessToken.getTokenValue()); + accessTokenData.put("tokenType", accessToken.getTokenType().getValue()); + accessTokenData.put("issuedAt", accessToken.getIssuedAt().toString()); + accessTokenData.put("expiresAt", accessToken.getExpiresAt().toString()); + accessTokenData.put("scopes", new ArrayList<>(accessToken.getScopes())); + clientData.put("accessToken", accessTokenData); + + // 序列化 RefreshToken(如果有) + OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken(); + if (refreshToken != null) { + Map refreshTokenData = new HashMap<>(); + refreshTokenData.put("tokenValue", refreshToken.getTokenValue()); + if (refreshToken.getIssuedAt() != null) { + refreshTokenData.put("issuedAt", refreshToken.getIssuedAt().toString()); + } + if (refreshToken.getExpiresAt() != null) { + refreshTokenData.put("expiresAt", refreshToken.getExpiresAt().toString()); + } + clientData.put("refreshToken", refreshTokenData); + } + + // 存储到 Redis + redisTemplate.opsForValue().set(key, clientData); + + // 设置过期时间(根据 AccessToken 的过期时间) + Duration expiresIn = Duration.between(Instant.now(), accessToken.getExpiresAt()); + if (!expiresIn.isNegative()) { + redisTemplate.expire(key, 7, TimeUnit.DAYS); + } + + } catch (Exception e) { + throw new RuntimeException("Failed to save OAuth2AuthorizedClient to Redis", e); + } + } + + @Override + public void removeAuthorizedClient( + String clientRegistrationId, + Authentication principal, + HttpServletRequest request, + HttpServletResponse response) { + + if (principal == null) { + return; + } + + String principalName = principal.getName(); + String key = buildClientKey(principalName, clientRegistrationId); + + // 删除客户端数据 + redisTemplate.delete(key); + } + + private String buildClientKey(String principalName, String clientRegistrationId) { + return CacheConstants.OAUTH2_CLIENT_KEY_PREFIX + principalName + ":" + clientRegistrationId; + } + + + private OAuth2AccessToken rebuildAccessToken(Map data) { + String tokenValue = (String) data.get("tokenValue"); + OAuth2AccessToken.TokenType tokenType = OAuth2AccessToken.TokenType.BEARER; + if (data.containsKey("tokenType")) { + String typeStr = (String) data.get("tokenType"); + tokenType = new OAuth2AccessToken.TokenType(typeStr); + } + + Instant issuedAt = Instant.parse((String) data.get("issuedAt")); + Instant expiresAt = Instant.parse((String) data.get("expiresAt")); + + @SuppressWarnings("unchecked") + Set scopes = new HashSet<>((List) data.get("scopes")); + + return new OAuth2AccessToken(tokenType, tokenValue, issuedAt, expiresAt, scopes); + } + + private OAuth2RefreshToken rebuildRefreshToken(Map data) { + String tokenValue = (String) data.get("tokenValue"); + Instant issuedAt = data.containsKey("issuedAt") ? + Instant.parse((String) data.get("issuedAt")) : null; + Instant expiresAt = data.containsKey("expiresAt") ? + Instant.parse((String) data.get("expiresAt")) : null; + + return new OAuth2RefreshToken(tokenValue, issuedAt, expiresAt); + } + + /** + * 获取用户的所有客户端 + */ + public List findByPrincipalName(String principalName) { + String pattern = CacheConstants.OAUTH2_CLIENT_KEY_PREFIX + principalName + ":*"; + Set keys = redisTemplate.keys(pattern); + + List clients = new ArrayList<>(); + for (String key : keys) { + String clientRegistrationId = extractClientRegistrationId(key); + // 这里需要 principal,简化处理 + // 实际使用时可能需要调整 + } + + return clients; + } + + /** + * 清理过期的客户端 + */ + public void cleanupExpiredClients() { + // 可以通过 Redis 的过期策略自动清理 + // 也可以手动扫描并删除过期的 token + String pattern = CacheConstants.OAUTH2_CLIENT_KEY_PREFIX + "*"; + Set keys = redisTemplate.keys(pattern); + + for (String key : keys) { + Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS); + if (ttl != null && ttl <= 0) { + redisTemplate.delete(key); + } + } + } + + private String extractClientRegistrationId(String key) { + return key.substring(key.lastIndexOf(":") + 1); + } +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientService.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientService.java new file mode 100644 index 0000000..4a1eb53 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientService.java @@ -0,0 +1,50 @@ +package org.lingniu.sdk.service; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.stereotype.Component; + +import java.util.Collections; + +@Component +public class RedisOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService { + + private final RedisOAuth2AuthorizedClientRepository repository; + + public RedisOAuth2AuthorizedClientService(RedisOAuth2AuthorizedClientRepository repository) { + this.repository = repository; + } + + @Override + public T loadAuthorizedClient( + String clientRegistrationId, String principalName) { + + // 从 Redis 加载 + Authentication authentication = createAuthentication(principalName); + return repository.loadAuthorizedClient(clientRegistrationId, authentication, null); + } + + @Override + public void saveAuthorizedClient( + OAuth2AuthorizedClient authorizedClient, Authentication principal) { + + repository.saveAuthorizedClient(authorizedClient, principal, null, null); + } + + @Override + public void removeAuthorizedClient(String clientRegistrationId, String principalName) { + Authentication authentication = createAuthentication(principalName); + repository.removeAuthorizedClient(clientRegistrationId, authentication, null, null); + } + + + private Authentication createAuthentication(String principalName) { + return new UsernamePasswordAuthenticationToken( + principalName, + null, + Collections.emptyList() + ); + } +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisRefreshTokenService.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisRefreshTokenService.java new file mode 100644 index 0000000..2454ae5 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/RedisRefreshTokenService.java @@ -0,0 +1,81 @@ +package org.lingniu.sdk.service; + +import lombok.extern.slf4j.Slf4j; +import org.lingniu.sdk.common.redis.RedisCache; +import org.lingniu.sdk.constant.CacheConstants; +import org.lingniu.sdk.model.token.RefreshTokenInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Map; + +@Component +@Slf4j +public class RedisRefreshTokenService { + + @Autowired + private RedisCache redisCache; + + private final long REFRESH_TOKEN_EXPIRE = 30 * 24 * 3600L; // 30天 + + /** + * 存储Refresh Token到Redis Hash + */ + public void storeRefreshToken(RefreshTokenInfo tokenInfo) { + if(tokenInfo==null){ + return; + } + String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, tokenInfo.getTokenValue()); + + redisCache.setCacheMap(key,tokenInfo.toMap()); + Instant expiresAt = tokenInfo.getExpiresAt(); + long expire = REFRESH_TOKEN_EXPIRE; + if(expiresAt!=null){ + expire = expiresAt.getEpochSecond() - Instant.now().getEpochSecond(); + } + + redisCache.expire(key,expire); + // 维护用户会话列表 + String userSessionsKey = String.format(CacheConstants.USER_SESSIONS, tokenInfo.getUsername()); + redisCache.setCacheSet(userSessionsKey,tokenInfo.getTokenValue()); + redisCache.expire(userSessionsKey,expire); + } + + /** + * 获取Refresh Token信息 + */ + public RefreshTokenInfo getRefreshTokenInfo(String token) { + String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, token); + Map cacheMap = redisCache.getCacheMap(key); + if(cacheMap!=null){ + return RefreshTokenInfo.fromMap(cacheMap); + } + return null; + } + + /** + * 更新Refresh Token最后使用时间 + */ + public void updateRefreshToken(RefreshTokenInfo tokenInfo) { + if(tokenInfo==null){ + return; + } + String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, tokenInfo.getTokenValue()); + redisCache.setCacheMap(key,tokenInfo.toUpdateMap()); + } + + /** + * 作废 删除 + * @param tokenInfo + */ + public void revokeRefreshToken(RefreshTokenInfo tokenInfo) { + if(tokenInfo==null){ + return; + } + String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, tokenInfo.getTokenValue()); + String userSessionsKey = String.format(CacheConstants.USER_SESSIONS, tokenInfo.getUsername()); + redisCache.deleteCacheSetValue(userSessionsKey,tokenInfo.getTokenValue()); + redisCache.deleteObject(key); + } +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/TokenService.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/TokenService.java new file mode 100644 index 0000000..d2c3776 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/service/TokenService.java @@ -0,0 +1,240 @@ +package org.lingniu.sdk.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.lingniu.sdk.model.token.AccessTokenInfo; +import org.lingniu.sdk.model.token.RefreshTokenInfo; +import org.lingniu.sdk.model.token.TokenInfo; +import org.lingniu.sdk.model.user.UserInfo; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.rememberme.InvalidCookieException; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; + +@Component +@Slf4j +public class TokenService { + private final ObjectMapper objectMapper; + // 令牌有效期(默认30分钟)单位分钟 + @Value("${token.accessToken.expireTime:30}") + private int accessTokenExpireTime; + // 刷新令牌有效期(默认24小时) 单位小时 + @Value("${token.refreshToken.expireTime:168}") + private int refreshTokenExpireTime; + @Value("${token.header:Authorization}") + private String tokenHeader; + private final RedisRefreshTokenService refreshTokenService; + + private final RedisAccessTokenService accessTokenService; + + + private final RedisOAuth2AuthorizedClientService redisOAuth2AuthorizedClientService; + public TokenService(RedisRefreshTokenService refreshTokenService, RedisAccessTokenService accessTokenService, RedisOAuth2AuthorizedClientService redisOAuth2AuthorizedClientService, ObjectMapper objectMapper) { + this.refreshTokenService = refreshTokenService; + this.accessTokenService = accessTokenService; + this.redisOAuth2AuthorizedClientService = redisOAuth2AuthorizedClientService; + this.objectMapper = objectMapper; + } + + public TokenInfo createToken(String username) throws JsonProcessingException { + String accessToken = UUID.randomUUID().toString().replace("-", ""); + String refreshToken = UUID.randomUUID().toString().replace("-", ""); + Instant issuedAt = Instant.now(); + Instant accessExpiresAt = issuedAt.plusSeconds(accessTokenExpireTime * 60L); + Instant refreshExpiresAt = issuedAt.plus(refreshTokenExpireTime, ChronoUnit.HOURS); + AccessTokenInfo accessTokenInfo = AccessTokenInfo.builder() + .tokenValue(accessToken) + .username(username) + .issuedAt(issuedAt) + .expiresAt(accessExpiresAt) + .refreshTokenId(refreshToken) + .build(); + RefreshTokenInfo refreshTokenInfo = RefreshTokenInfo.builder() + .tokenValue(refreshToken) + .username(username) + .accessToken(accessToken) + .createdAt(issuedAt) + .lastUsedAt(refreshExpiresAt) + .accessTokenCount(1) + .usageCount(0) + .build(); + return new TokenInfo(accessTokenInfo,refreshTokenInfo); + } + public void storeTokenInfo(TokenInfo tokenInfo){ + accessTokenService.storeAccessToken(tokenInfo.getAccessTokenInfo()); + + refreshTokenService.storeRefreshToken(tokenInfo.getRefreshTokenInfo()); + } + public void setAccessTokenHeader(HttpServletResponse response,String accessToken){ + response.addHeader(tokenHeader,accessToken); + + } + + public void setRefreshTokenCookie(HttpServletResponse response, TokenInfo tokenInfo) { + String refreshToken = tokenInfo.getRefreshTokenInfo().getTokenValue(); +// Cookie cookie = new Cookie("refresh_token", refreshToken); +// cookie.setHttpOnly(true); +// cookie.setSecure(true); // 生产环境设为true +// cookie.setPath("/"); +// cookie.setMaxAge(refreshTokenExpireTime * 60 * 60); +// cookie.setDomain(".lingniu.com"); // 设置域名 + + // 添加SameSite属性 + response.addHeader("Set-Cookie", + String.format("app_refresh_token=%s; HttpOnly; Secure; Path=/; Max-Age=%d; SameSite=Strict", + refreshToken, refreshTokenExpireTime * 60 * 60)); + } + public String getCookieRefreshToken(HttpServletRequest request){ + Cookie[] cookies = request.getCookies(); + return Arrays.stream(cookies) + .filter(cookie -> "app_refresh_token".equals(cookie.getName())) + .map(Cookie::getValue) + .findFirst() + .orElse(null); + } + public boolean validateAccessToken(HttpServletRequest request){ + String accessToken = request.getHeader(tokenHeader); + return accessTokenService.validateAccessToken(accessToken); + } + + public AccessTokenInfo getAccessTokenInfo(HttpServletRequest request){ + String accessToken = request.getHeader(tokenHeader); + return accessTokenService.getAccessTokenInfo(accessToken); + } + public RefreshTokenInfo getRefreshTokenInfo(HttpServletRequest request){ + String accessToken = getCookieRefreshToken(request); + return refreshTokenService.getRefreshTokenInfo(accessToken); + } + + public AccessTokenInfo refreshToken(HttpServletRequest request,HttpServletResponse response) throws IOException { + String accessToken = request.getHeader(tokenHeader); + String cookieRefreshToken = getCookieRefreshToken(request); + RefreshTokenInfo refreshTokenInfo = refreshTokenService.getRefreshTokenInfo(cookieRefreshToken); + if(refreshTokenInfo == null || !refreshTokenInfo.isValid()){ + log.error("token 已刷新"); + return null; + } + if(refreshTokenInfo.getAccessToken()!=null && !refreshTokenInfo.getAccessToken().equals(accessToken)){ + log.error("token 已刷新"); + } + String clientRegistrationId = refreshTokenInfo.getClientRegistrationId(); + String username = refreshTokenInfo.getUsername(); + OAuth2AuthorizedClient oAuth2AuthorizedClient = redisOAuth2AuthorizedClientService.loadAuthorizedClient(clientRegistrationId, username); + if(oAuth2AuthorizedClient==null){ + log.error("idp client is expire"); + return null; + } + if(hasTokenExpired(oAuth2AuthorizedClient.getAccessToken())){ + RefreshTokenOAuth2AuthorizedClientProvider refreshTokenOAuth2AuthorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider(); + OAuth2AuthorizationContext oAuth2AuthorizationContext = OAuth2AuthorizationContext.withAuthorizedClient(oAuth2AuthorizedClient).principal(createAuthentication(username)).build(); + oAuth2AuthorizedClient = refreshTokenOAuth2AuthorizedClientProvider.authorize(oAuth2AuthorizationContext); + redisOAuth2AuthorizedClientService.saveAuthorizedClient(oAuth2AuthorizedClient,createAuthentication(username)); + } + if(oAuth2AuthorizedClient==null){ + log.error("idp client is expire"); + return null; + } + DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService(); + + OAuth2UserRequest oAuth2UserRequest = new OAuth2UserRequest(oAuth2AuthorizedClient.getClientRegistration(),oAuth2AuthorizedClient.getAccessToken()); + + OAuth2User oAuth2User = defaultOAuth2UserService.loadUser(oAuth2UserRequest); + String s = objectMapper.writeValueAsString(oAuth2User.getAttributes()); + + String accessTokenNew = UUID.randomUUID().toString().replace("-", ""); + Instant issuedAt = Instant.now(); + Instant accessExpiresAt = issuedAt.plusSeconds(accessTokenExpireTime * 60L); + AccessTokenInfo accessTokenInfo = AccessTokenInfo.builder() + .tokenValue(accessTokenNew) + .username(refreshTokenInfo.getUsername()) + .username(refreshTokenInfo.getUsername()) + .issuedAt(issuedAt) + .expiresAt(accessExpiresAt) + .additionalInfo(s) + .clientRegistrationId(refreshTokenInfo.getClientRegistrationId()) + .refreshTokenId(refreshTokenInfo.getTokenValue()) + .build(); + accessTokenService.storeAccessToken(accessTokenInfo); + + refreshTokenInfo.incrementUsage(); + refreshTokenInfo.incrementAccessTokenUsage(accessTokenNew); + refreshTokenService.updateRefreshToken(refreshTokenInfo); + setAccessTokenHeader(response,accessTokenNew); + return accessTokenInfo; + } + + public UserInfo convertPrincipal(AccessTokenInfo accessTokenInfo) throws JsonProcessingException { + return objectMapper.convertValue(objectMapper.readValue(accessTokenInfo.getAdditionalInfo(), Map.class), UserInfo.class); + } + public void revokeToken(HttpServletRequest request,HttpServletResponse response){ + String accessToken = request.getHeader(tokenHeader); + String cookieRefreshToken = getCookieRefreshToken(request); + AccessTokenInfo accessTokenInfo = accessTokenService.getAccessTokenInfo(accessToken); + RefreshTokenInfo refreshTokenInfo = refreshTokenService.getRefreshTokenInfo(cookieRefreshToken); + Instant now = Instant.now(); + if(accessTokenInfo!=null){ + accessTokenService.revokeAccessToken(accessTokenInfo); + } + if(refreshTokenInfo!=null){ + refreshTokenService.revokeRefreshToken(refreshTokenInfo); + } + clearRefreshTokenCookie(response); + } + public void clearToken(HttpServletRequest request){ + RefreshTokenInfo refreshTokenInfo = getRefreshTokenInfo(request); + AccessTokenInfo accessTokenInfo = getAccessTokenInfo(request); + accessTokenService.revokeAccessToken(accessTokenInfo); + refreshTokenService.revokeRefreshToken(refreshTokenInfo); + if(refreshTokenInfo!=null){ + AccessTokenInfo accessTokenInfo1 = accessTokenService.getAccessTokenInfo(refreshTokenInfo.getAccessToken()); + accessTokenService.revokeAccessToken(accessTokenInfo1); + } +// redisOAuth2AuthorizedClientService.removeAuthorizedClient(refreshTokenInfo.getClientRegistrationId(),refreshTokenInfo.getUsername()); + } + + public void clearRefreshTokenCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("refresh_token", null); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(0); + + response.addCookie(cookie); + } + private final Duration clockSkew = Duration.ofSeconds(60); + + private final Clock clock = Clock.systemUTC(); + private boolean hasTokenExpired(OAuth2Token token) { + return this.clock.instant().isAfter(Objects.requireNonNull(token.getExpiresAt()).minus(this.clockSkew)); + } + private Authentication createAuthentication(String principalName) { + return new UsernamePasswordAuthenticationToken( + principalName, + null, + Collections.emptyList() + ); + } +} diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/utils/HttpClientUtils.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/utils/HttpClientUtils.java new file mode 100644 index 0000000..020f8cc --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/utils/HttpClientUtils.java @@ -0,0 +1,111 @@ +package org.lingniu.sdk.utils; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class HttpClientUtils { + + private static final HttpClient DEFAULT_CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .version(HttpClient.Version.HTTP_2) + .build(); + + // GET请求 + public static String get(String url, Map headers) throws Exception { + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET(); + + if (headers != null) { + headers.forEach(builder::header); + } + + HttpRequest request = builder.build(); + + HttpResponse response = DEFAULT_CLIENT.send( + request, + HttpResponse.BodyHandlers.ofString() + ); + + if (response.statusCode() >= 200 && response.statusCode() < 300) { + return response.body(); + } else { + throw new RuntimeException("HTTP Error: " + response.statusCode()); + } + } + + // POST请求 + public static String post(String url, String body, Map headers) + throws Exception { + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .POST(HttpRequest.BodyPublishers.ofString(body)); + + if (headers != null) { + headers.forEach(builder::header); + } + + HttpRequest request = builder.build(); + + HttpResponse response = DEFAULT_CLIENT.send( + request, + HttpResponse.BodyHandlers.ofString() + ); + + if (response.statusCode() >= 200 && response.statusCode() < 300) { + return response.body(); + } else { + throw new RuntimeException("HTTP Error: " + response.statusCode()); + } + } + + // POST JSON请求 + public static String postJson(String url, String json) throws Exception { + return post(url, json, Map.of( + "Content-Type", "application/json", + "Accept", "application/json" + )); + } + + // 异步GET请求 + public static CompletableFuture getAsync(String url) { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + return DEFAULT_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(response -> { + if (response.statusCode() >= 200 && response.statusCode() < 300) { + return response.body(); + } else { + throw new RuntimeException("HTTP Error: " + response.statusCode()); + } + }); + } + + // 测试示例 + public static void main(String[] args) { + try { + // 同步GET + String response = get( + "https://jsonplaceholder.typicode.com/posts/1", + Map.of("User-Agent", "Java Client") + ); + System.out.println("GET Response: " + response); + + // 异步GET + getAsync("https://jsonplaceholder.typicode.com/posts/2") + .thenAccept(r -> System.out.println("Async Response: " + r)) + .join(); + + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/web/UserController.java b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/web/UserController.java new file mode 100644 index 0000000..c3e3630 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/src/main/java/org/lingniu/sdk/web/UserController.java @@ -0,0 +1,41 @@ +package org.lingniu.sdk.web; + +import com.alibaba.fastjson2.JSON; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.lingniu.sdk.model.base.CommonResult; +import org.lingniu.sdk.model.user.UserInfo; +import org.lingniu.sdk.utils.HttpClientUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RequestMapping("/idp") +@RestController +public class UserController { + + private final OAuth2ClientProperties oAuth2ClientProperties; + private final ObjectMapper objectMapper; + + public UserController(OAuth2ClientProperties oAuth2ClientProperties, ObjectMapper objectMapper) { + this.oAuth2ClientProperties = oAuth2ClientProperties; + this.objectMapper = objectMapper; + } + + @GetMapping("/routes") + public CommonResult getUserMenu(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) throws Exception { + OAuth2AccessToken.TokenType tokenType = oAuth2AuthorizedClient.getAccessToken().getTokenType(); + String tokenValue = oAuth2AuthorizedClient.getAccessToken().getTokenValue(); + + String s = HttpClientUtils.get(oAuth2ClientProperties.getProvider().get("idp").getUserInfoUri().replace("userinfo","idp/getRouters"), + Map.of("Authorization",tokenType.getValue() + " " + tokenValue,"Accept","application/json")); + return CommonResult.success(objectMapper.readValue(s,Map.class).get("data")); + } +} diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/common/redis/RedisCache.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/common/redis/RedisCache.class new file mode 100644 index 0000000..04eba5e Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/common/redis/RedisCache.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/common/serializer/FastJson2JsonRedisSerializer.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/common/serializer/FastJson2JsonRedisSerializer.class new file mode 100644 index 0000000..6c65e8b Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/common/serializer/FastJson2JsonRedisSerializer.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/config/JacksonConfiguration.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/config/JacksonConfiguration.class new file mode 100644 index 0000000..611f212 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/config/JacksonConfiguration.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/config/SdkRedisConfig.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/config/SdkRedisConfig.class new file mode 100644 index 0000000..a0c3426 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/config/SdkRedisConfig.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/config/SecurityConfig.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/config/SecurityConfig.class new file mode 100644 index 0000000..8437408 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/config/SecurityConfig.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/constant/CacheConstants.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/constant/CacheConstants.class new file mode 100644 index 0000000..01ad05f Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/constant/CacheConstants.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/constant/Constants.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/constant/Constants.class new file mode 100644 index 0000000..3e46510 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/constant/Constants.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/constant/UserConstants.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/constant/UserConstants.class new file mode 100644 index 0000000..ce9fe7e Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/constant/UserConstants.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/filter/IdpAuthenticationFilter.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/filter/IdpAuthenticationFilter.class new file mode 100644 index 0000000..af661b5 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/filter/IdpAuthenticationFilter.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/handler/LoginSuccessHandler.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/handler/LoginSuccessHandler.class new file mode 100644 index 0000000..6f5e345 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/handler/LoginSuccessHandler.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/handler/LogoutIdpSuccessHandler.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/handler/LogoutIdpSuccessHandler.class new file mode 100644 index 0000000..b9b749c Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/handler/LogoutIdpSuccessHandler.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/handler/RedirectHandler.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/handler/RedirectHandler.class new file mode 100644 index 0000000..7adc1e1 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/handler/RedirectHandler.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/base/CommonResult.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/base/CommonResult.class new file mode 100644 index 0000000..f6b5e6c Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/base/CommonResult.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/AccessTokenInfo$AccessTokenInfoBuilder.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/AccessTokenInfo$AccessTokenInfoBuilder.class new file mode 100644 index 0000000..8f6056a Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/AccessTokenInfo$AccessTokenInfoBuilder.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/AccessTokenInfo.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/AccessTokenInfo.class new file mode 100644 index 0000000..e161024 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/AccessTokenInfo.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/RefreshTokenInfo$RefreshTokenInfoBuilder.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/RefreshTokenInfo$RefreshTokenInfoBuilder.class new file mode 100644 index 0000000..bc88f94 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/RefreshTokenInfo$RefreshTokenInfoBuilder.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/RefreshTokenInfo.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/RefreshTokenInfo.class new file mode 100644 index 0000000..2be8995 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/RefreshTokenInfo.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/TokenInfo$TokenInfoBuilder.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/TokenInfo$TokenInfoBuilder.class new file mode 100644 index 0000000..b1968f4 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/TokenInfo$TokenInfoBuilder.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/TokenInfo.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/TokenInfo.class new file mode 100644 index 0000000..520a0d8 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/token/TokenInfo.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/DataPermission.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/DataPermission.class new file mode 100644 index 0000000..75279f4 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/DataPermission.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/UserDept.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/UserDept.class new file mode 100644 index 0000000..09a63e5 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/UserDept.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/UserInfo.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/UserInfo.class new file mode 100644 index 0000000..7cdeafc Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/UserInfo.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/UserPost.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/UserPost.class new file mode 100644 index 0000000..a3bd049 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/model/user/UserPost.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisAccessTokenService.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisAccessTokenService.class new file mode 100644 index 0000000..a8c6f91 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisAccessTokenService.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientRepository$1.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientRepository$1.class new file mode 100644 index 0000000..8b87cca Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientRepository$1.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientRepository.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientRepository.class new file mode 100644 index 0000000..adf8f3e Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientRepository.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientService.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientService.class new file mode 100644 index 0000000..26237e6 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisOAuth2AuthorizedClientService.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisRefreshTokenService.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisRefreshTokenService.class new file mode 100644 index 0000000..a1d9d8f Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/RedisRefreshTokenService.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/TokenService.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/TokenService.class new file mode 100644 index 0000000..642839b Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/service/TokenService.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/utils/HttpClientUtils.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/utils/HttpClientUtils.class new file mode 100644 index 0000000..0978ae2 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/utils/HttpClientUtils.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/web/UserController.class b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/web/UserController.class new file mode 100644 index 0000000..7ca9ec6 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/classes/org/lingniu/sdk/web/UserController.class differ diff --git a/sdk/backend/oauth2-login-sdk/target/maven-archiver/pom.properties b/sdk/backend/oauth2-login-sdk/target/maven-archiver/pom.properties new file mode 100644 index 0000000..e41e776 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/target/maven-archiver/pom.properties @@ -0,0 +1,5 @@ +#Generated by Maven +#Sun Feb 08 09:46:13 CST 2026 +groupId=org.lingniu +artifactId=oauth2-login-sdk +version=1.0-SNAPSHOT diff --git a/sdk/backend/oauth2-login-sdk/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/sdk/backend/oauth2-login-sdk/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..9f9d485 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,30 @@ +org\lingniu\sdk\model\token\AccessTokenInfo.class +org\lingniu\sdk\service\RedisRefreshTokenService.class +org\lingniu\sdk\constant\UserConstants.class +org\lingniu\sdk\model\token\TokenInfo.class +org\lingniu\sdk\common\redis\RedisCache.class +org\lingniu\sdk\model\base\CommonResult.class +org\lingniu\sdk\model\user\UserPost.class +org\lingniu\sdk\service\RedisOAuth2AuthorizedClientService.class +org\lingniu\sdk\constant\CacheConstants.class +org\lingniu\sdk\service\RedisOAuth2AuthorizedClientRepository$1.class +org\lingniu\sdk\service\RedisAccessTokenService.class +org\lingniu\sdk\model\user\DataPermission.class +org\lingniu\sdk\service\RedisOAuth2AuthorizedClientRepository.class +org\lingniu\sdk\web\UserController.class +org\lingniu\sdk\model\user\UserInfo.class +org\lingniu\sdk\utils\HttpClientUtils.class +org\lingniu\sdk\handler\LoginSuccessHandler.class +org\lingniu\sdk\model\token\TokenInfo$TokenInfoBuilder.class +org\lingniu\sdk\constant\Constants.class +org\lingniu\sdk\service\TokenService.class +org\lingniu\sdk\model\user\UserDept.class +org\lingniu\sdk\model\token\AccessTokenInfo$AccessTokenInfoBuilder.class +org\lingniu\sdk\model\token\RefreshTokenInfo$RefreshTokenInfoBuilder.class +org\lingniu\sdk\config\RedisConfig.class +org\lingniu\sdk\config\JacksonConfiguration.class +org\lingniu\sdk\filter\IdpAuthenticationFilter.class +org\lingniu\sdk\common\serializer\FastJson2JsonRedisSerializer.class +org\lingniu\sdk\handler\LogoutIdpSuccessHandler.class +org\lingniu\sdk\model\token\RefreshTokenInfo.class +org\lingniu\sdk\config\SecurityConfig.class diff --git a/sdk/backend/oauth2-login-sdk/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/sdk/backend/oauth2-login-sdk/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..d2152f9 --- /dev/null +++ b/sdk/backend/oauth2-login-sdk/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,26 @@ +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\constant\CacheConstants.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\user\DataPermission.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\service\TokenService.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\token\TokenInfo.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\utils\HttpClientUtils.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\user\UserInfo.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\service\RedisOAuth2AuthorizedClientService.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\constant\Constants.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\service\RedisAccessTokenService.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\config\JacksonConfiguration.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\config\RedisConfig.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\base\CommonResult.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\user\UserDept.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\config\SecurityConfig.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\filter\IdpAuthenticationFilter.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\user\UserPost.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\common\redis\RedisCache.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\token\RefreshTokenInfo.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\web\UserController.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\service\RedisOAuth2AuthorizedClientRepository.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\handler\LogoutIdpSuccessHandler.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\service\RedisRefreshTokenService.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\constant\UserConstants.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\handler\LoginSuccessHandler.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\token\AccessTokenInfo.java +D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\common\serializer\FastJson2JsonRedisSerializer.java diff --git a/sdk/backend/oauth2-login-sdk/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/sdk/backend/oauth2-login-sdk/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/sdk/backend/oauth2-login-sdk/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/sdk/backend/oauth2-login-sdk/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/sdk/backend/oauth2-login-sdk/target/oauth2-login-sdk-1.0-SNAPSHOT.jar b/sdk/backend/oauth2-login-sdk/target/oauth2-login-sdk-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..6f38072 Binary files /dev/null and b/sdk/backend/oauth2-login-sdk/target/oauth2-login-sdk-1.0-SNAPSHOT.jar differ diff --git a/sdk/frontend/oauth2-login-sdk/README.md b/sdk/frontend/oauth2-login-sdk/README.md new file mode 100644 index 0000000..24d28b8 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/README.md @@ -0,0 +1,533 @@ + +## 安装 + +```bash +npm install unified-login-sdk --save +# 或 +yarn add unified-login-sdk +``` + +## 快速开始 + +### 基本使用 + +```typescript +import unifiedLoginSDK from 'unified-login-sdk'; + +// 初始化配置 +unifiedLoginSDK.init({ + clientId: 'your-client-id', + authorizationEndpoint: 'https://auth.example.com/authorize', + tokenEndpoint: 'https://auth.example.com/token', + userInfoEndpoint: 'https://auth.example.com/userinfo', + redirectUri: 'https://your-app.example.com/callback', + storageType: 'localStorage', + autoRefreshToken: true, + tenantId: 'your-tenant-id' // 可选,会自动添加到请求头中的tenant-id字段 +}); + +// 登录 +document.getElementById('login-btn')?.addEventListener('click', () => { + unifiedLoginSDK.login(); +}); + +// 处理回调 +if (unifiedLoginSDK.isAuthenticated()) { + // 已登录,获取用户信息 + unifiedLoginSDK.getUserInfo().then(userInfo => { + console.log('User info:', userInfo); + }); +} else if (unifiedLoginSDK.isCallback()) { + // 处理授权回调 + unifiedLoginSDK.handleCallback().then(userInfo => { + console.log('Login successful:', userInfo); + // 跳转到首页 + window.location.href = '/'; + }).catch(error => { + console.error('Login failed:', error); + }); +} + +// 退出登录 +document.getElementById('logout-btn')?.addEventListener('click', () => { + unifiedLoginSDK.logout().then(() => { + console.log('Logout successful'); + window.location.href = '/login'; + }); +}); +``` + +## 核心功能 + +### 初始化配置 + +```typescript +unifiedLoginSDK.init({ + clientId: 'your-client-id', + clientSecret: 'your-client-secret', // 可选,某些场景下需要 + authorizationEndpoint: 'https://auth.example.com/authorize', + tokenEndpoint: 'https://auth.example.com/token', + userInfoEndpoint: 'https://auth.example.com/userinfo', + redirectUri: 'https://your-app.example.com/callback', + storageType: 'localStorage', // 可选,默认localStorage + autoRefreshToken: true, // 可选,默认true + permissionsEndpoint: 'https://auth.example.com/permissions' // 可选,权限端点 +}); +``` + +### 登录流程 + +1. 调用`login()`方法跳转到授权页面 +2. 用户在授权页面登录并授权 +3. 授权服务器重定向到配置的`redirectUri` +4. 调用`handleCallback()`方法处理授权回调,获取用户信息 + +### Token管理 + +```typescript +// 获取访问令牌 +const accessToken = unifiedLoginSDK.getAccessToken(); + +// 刷新令牌 +unifiedLoginSDK.refreshToken().then(() => { + console.log('Token refreshed'); +}).catch(error => { + console.error('Failed to refresh token:', error); +}); + +// 检查是否已认证 +const isAuthenticated = unifiedLoginSDK.isAuthenticated(); +``` + +### 用户信息管理 + +```typescript +// 获取用户信息 +unifiedLoginSDK.getUserInfo().then(userInfo => { + console.log('User info:', userInfo); +}); + +// 获取用户权限列表 +unifiedLoginSDK.getPermissions().then(permissions => { + console.log('Permissions:', permissions); +}); +``` + +### 事件监听 + +```typescript +// 监听登录事件 +unifiedLoginSDK.on('login', () => { + console.log('User logged in'); +}); + +// 监听退出事件 +unifiedLoginSDK.on('logout', () => { + console.log('User logged out'); +}); + +// 监听Token过期事件 +unifiedLoginSDK.on('tokenExpired', () => { + console.log('Token expired'); + // 可以在这里执行自定义逻辑,如跳转到登录页 + unifiedLoginSDK.login(); +}); + +// 移除事件监听 +const handleLogin = () => console.log('User logged in'); +unifiedLoginSDK.on('login', handleLogin); +unifiedLoginSDK.off('login', handleLogin); +``` + +## 框架集成 + +### Vue 2 + +```javascript +// main.js +import Vue from 'vue'; +import { createVuePlugin } from 'unified-login-sdk'; +import App from './App.vue'; +import router from './router'; + +// 创建Vue插件 +const vuePlugin = createVuePlugin('localStorage'); + +// 安装插件 +Vue.use(vuePlugin, { + config: { + clientId: 'your-client-id', + authorizationEndpoint: 'https://auth.example.com/authorize', + tokenEndpoint: 'https://auth.example.com/token', + userInfoEndpoint: 'https://auth.example.com/userinfo', + redirectUri: 'https://your-app.example.com/callback' + } +}); + +new Vue({ + router, + render: h => h(App) +}).$mount('#app'); +``` + +在组件中使用: + +```vue + + + +``` + +### Vue 3 + +```javascript +// main.js +import { createApp } from 'vue'; +import { createVuePlugin } from 'unified-login-sdk'; +import App from './App.vue'; +import router from './router'; + +// 创建Vue插件 +const vuePlugin = createVuePlugin('localStorage'); + +const app = createApp(App); + +// 安装插件 +app.use(vuePlugin, { + config: { + clientId: 'your-client-id', + authorizationEndpoint: 'https://auth.example.com/authorize', + tokenEndpoint: 'https://auth.example.com/token', + userInfoEndpoint: 'https://auth.example.com/userinfo', + redirectUri: 'https://your-app.example.com/callback' + } +}); + +app.use(router); +app.mount('#app'); +``` + +在组件中使用(Composition API): + +```vue + + + +``` +``` + +## API参考 + +### 初始化 + +```typescript +init(config: SDKConfig): void +``` + +初始化SDK配置。 + +### 登录 + +```typescript +login(redirectUri?: string): void +``` + +触发登录流程,可选参数`redirectUri`可覆盖初始化时的配置。 + +### 退出登录 + +```typescript +logout(): Promise +``` + +退出登录,清除本地存储的Token和用户信息。 + +### 处理授权回调 + +```typescript +handleCallback(): Promise +``` + +处理授权回调,获取用户信息。 + +### 获取用户信息 + +```typescript +getUserInfo(): Promise +``` + +获取用户基本信息。 + +### 获取用户权限列表 + +```typescript +getPermissions(): Promise +``` + +获取用户权限列表。 + +### 检查是否已认证 + +```typescript +isAuthenticated(): boolean +``` + +检查用户是否已认证。 + +### 获取访问令牌 + +```typescript +getAccessToken(): string | null +``` + +获取访问令牌。 + +### 刷新访问令牌 + +```typescript +refreshToken(): Promise +``` + +刷新访问令牌。 + +### 事件监听 + +```typescript +on(event: 'login' | 'logout' | 'tokenExpired', callback: Function): void +``` + +监听登录、退出或Token过期事件。 + +### 移除事件监听 + +```typescript +off(event: 'login' | 'logout' | 'tokenExpired', callback: Function): void +``` + +移除事件监听。 + +## 配置选项 + +| 选项 | 类型 | 必填 | 默认值 | 描述 | +|------|------|------|--------|------| +| clientId | string | 是 | - | 客户端ID | +| clientSecret | string | 否 | - | 客户端密钥,某些场景下需要 | +| authorizationEndpoint | string | 是 | - | 授权端点URL | +| tokenEndpoint | string | 是 | - | Token端点URL | +| userInfoEndpoint | string | 是 | - | 用户信息端点URL | +| redirectUri | string | 是 | - | 重定向URL | +| storageType | 'localStorage' 'sessionStorage' 'cookie' | 否 | 'localStorage' | Token存储类型 | +| autoRefreshToken | boolean | 否 | true | 是否自动刷新Token | +| permissionsEndpoint | string | 否 | - | 权限端点URL | +| stateLength | number | 否 | 32 | 状态参数长度 | +| tenantId | string | 否 | - | 租户ID,会自动添加到请求头中的tenant-id字段 | + +## 事件处理 + +| 事件 | 描述 | +|------|------| +| login | 用户登录成功时触发 | +| logout | 用户退出登录时触发 | +| tokenExpired | Token过期时触发 | + +## 路由守卫 + +### Vue路由守卫 + +```javascript +// router/index.js +import VueRouter from 'vue-router'; +import { Auth } from 'unified-login-sdk'; +import { Storage } from 'unified-login-sdk'; +import { RouterGuard } from 'unified-login-sdk'; + +const storage = new Storage('localStorage'); +const auth = new Auth(storage); +const routerGuard = new RouterGuard(auth); + +const router = new VueRouter({ + routes: [ + { + path: '/', + name: 'Home', + component: Home + }, + { + path: '/protected', + name: 'Protected', + component: Protected, + meta: { + auth: { + requiresAuth: true, + requiredPermissions: ['read:protected'] + } + } + } + ] +}); + +// 添加路由守卫 +router.beforeEach(routerGuard.createVueGuard()); + +export default router; +``` + + + +## 错误处理 + +### 网络错误处理 + +```typescript +try { + await unifiedLoginSDK.getUserInfo(); +} catch (error) { + if (error.name === 'HttpError') { + // 处理HTTP错误 + console.error('HTTP Error:', error.status, error.message); + if (error.status === 401) { + // 未授权,跳转到登录页 + unifiedLoginSDK.login(); + } else if (error.status === 403) { + // 权限不足 + window.location.href = '/403'; + } + } else { + // 处理其他错误 + console.error('Error:', error.message); + } +} +``` + +### Token失效处理 + +```typescript +// 监听Token过期事件 +unifiedLoginSDK.on('tokenExpired', () => { + console.log('Token expired'); + // 跳转到登录页 + unifiedLoginSDK.login(); +}); +``` + +## 最佳实践 + +1. **配置安全存储**:根据项目需求选择合适的存储类型,敏感信息建议使用cookie并设置secure和httpOnly标志。 + +2. **合理设置Token过期时间**:根据项目安全性要求设置合适的Token过期时间,建议access token过期时间较短,refresh token过期时间较长。 + +3. **使用路由守卫保护敏感路由**:对需要登录或特定权限的路由使用路由守卫进行保护。 + +4. **处理网络错误**:在调用SDK方法时,使用try-catch捕获并处理可能的错误。 + +5. **监听Token过期事件**:及时处理Token过期情况,避免用户体验下降。 + +6. **不要直接暴露clientSecret**:clientSecret应该只在后端使用,前端SDK尽量避免使用clientSecret。 + +7. **使用HTTPS**:确保所有与授权服务器的通信都使用HTTPS,避免Token被窃取。 + +8. **定期清理存储**:在用户退出登录时,确保清理所有相关存储的信息。 + +## 浏览器兼容性 + +- Chrome (推荐) +- Firefox +- Safari +- Edge + +## 许可证 + +MIT License diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/auth.d.ts b/sdk/frontend/oauth2-login-sdk/dist/core/auth.d.ts new file mode 100644 index 0000000..d96f68e --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/auth.d.ts @@ -0,0 +1,101 @@ +/** + * 认证核心逻辑 + * 实现OAuth2授权码模式的完整流程 + */ +import { EventType, RouterInfo, SDKConfig, UserInfo } from '../types'; +import { Storage } from '../utils/storage'; +/** + * 认证核心类 + */ +export declare class Auth { + private config; + private tokenManager; + private httpClient; + private storage; + private eventHandlers; + private userInfoCache; + /** + * 构造函数 + * @param storage 存储实例 + */ + constructor(storage: Storage); + /** + * 初始化SDK配置 + * @param config SDK配置选项 + */ + init(config: SDKConfig): void; + getToken(): string | null; + /** + * 触发登录流程 + * @param redirectUri 可选的重定向URL,覆盖初始化时的配置 + */ + login(redirectUri?: string): Promise; + /** + * 退出登录 + */ + logout(): Promise; + /** + * 处理授权回调 + * @returns Promise 用户信息 + */ + handleCallback(): Promise; + getRoutes(): Promise; + /** + * 获取用户信息 + * @returns UserInfo 用户信息 + */ + getUserInfo(): UserInfo; + /** + * 检查用户是否有指定角色 + * @param role 角色编码或角色编码列表 + * @returns Promise 是否有指定角色 + */ + hasRole(role: string | string[]): Promise; + /** + * 检查用户是否有所有指定角色 + * @param roles 角色编码列表 + * @returns Promise 是否有所有指定角色 + */ + hasAllRoles(roles: string[]): Promise; + /** + * 检查用户是否有指定权限 + * @param permission 权限标识或权限标识列表 + * @returns Promise 是否有指定权限 + */ + hasPermission(permission: string | string[]): Promise; + /** + * 检查用户是否有所有指定权限 + * @param permissions 权限标识列表 + * @returns Promise 是否有所有指定权限 + */ + hasAllPermissions(permissions: string[]): Promise; + /** + * 检查用户是否已认证 + * @returns boolean 是否已认证 + */ + isAuthenticated(): boolean; + /** + * 事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + on(event: EventType, callback: Function): void; + /** + * 移除事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + off(event: EventType, callback: Function): void; + /** + * 触发事件 + * @param event 事件类型 + * @param data 事件数据 + */ + private emit; + /** + * 检查当前URL是否为授权回调 + * @returns boolean 是否为授权回调 + */ + isCallback(): boolean; +} +//# sourceMappingURL=auth.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/auth.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/core/auth.d.ts.map new file mode 100644 index 0000000..7f669aa --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/auth.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAC,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAC,MAAM,UAAU,CAAC;AAGpE,OAAO,EAAC,OAAO,EAAC,MAAM,kBAAkB,CAAC;AAGzC;;GAEG;AACH,qBAAa,IAAI;IACf,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,aAAa,CAInB;IACF,OAAO,CAAC,aAAa,CAAyB;IAE9C;;;OAGG;gBACS,OAAO,EAAE,OAAO;IAQ5B;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IAM7B,QAAQ,IAAG,MAAM,GAAG,IAAI;IAIxB;;;OAGG;IACG,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAchD;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAe7B;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC/B,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;IAYtC;;;OAGG;IACF,WAAW,IAAI,QAAQ;IAOxB;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBxD;;;;OAIG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAWpD;;;;OAIG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBpE;;;;OAIG;IACG,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAYhE;;;OAGG;IACH,eAAe,IAAI,OAAO;IAK1B;;;;OAIG;IACH,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAI9C;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAI/C;;;;OAIG;IACH,OAAO,CAAC,IAAI;IAUZ;;;OAGG;IACH,UAAU,IAAI,OAAO;CAGtB"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/auth.js b/sdk/frontend/oauth2-login-sdk/dist/core/auth.js new file mode 100644 index 0000000..1fea028 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/auth.js @@ -0,0 +1,241 @@ +/** + * 认证核心逻辑 + * 实现OAuth2授权码模式的完整流程 + */ +import { TokenManager } from './token'; +import { HttpClient } from './http'; +import { buildQueryParams, isCallbackUrl, parseQueryParams } from '../utils/url'; +/** + * 认证核心类 + */ +export class Auth { + /** + * 构造函数 + * @param storage 存储实例 + */ + constructor(storage) { + this.config = null; + this.eventHandlers = { + login: [], + logout: [], + tokenExpired: [] + }; + this.userInfoCache = null; + this.storage = storage; + // 先创建HttpClient,初始时tokenManager为undefined + this.httpClient = new HttpClient(() => this.tokenManager.getToken() || null); + // 然后创建TokenManager + this.tokenManager = new TokenManager(storage); + } + /** + * 初始化SDK配置 + * @param config SDK配置选项 + */ + init(config) { + this.config = config; + // 设置租户ID到HTTP客户端 + this.httpClient.setTenantId(config.tenantId); + } + getToken() { + return this.tokenManager.getToken(); + } + /** + * 触发登录流程 + * @param redirectUri 可选的重定向URL,覆盖初始化时的配置 + */ + async login(redirectUri) { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const registrationId = this.config.registrationId || 'idp'; + const basepath = this.config.basepath || ''; + const path = `${basepath}/oauth2/authorization/${registrationId}`; + const tokenResponse = await this.httpClient.get(path, { needAuth: false }); + const redirect = tokenResponse.data.redirect_url; + const params = parseQueryParams(redirect); + this.storage.set(params.state, window.location.href); + window.location.href = redirect; + } + /** + * 退出登录 + */ + async logout() { + if (!this.config) { + throw new Error('SDK not initialized'); + } + // 清除本地存储的Token和用户信息 + this.tokenManager.clearToken(); + this.userInfoCache = null; + this.storage.remove('userInfo'); + const basepath = this.config.basepath || ''; + await this.httpClient.post(`${basepath}/logout`, null, { needAuth: true }); + // 触发退出事件 + this.emit('logout'); + window.location.href = this.config.idpLogoutUrl + '?redirect=' + this.config.homePage; + } + /** + * 处理授权回调 + * @returns Promise 用户信息 + */ + async handleCallback() { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const params = parseQueryParams(); + // 检查是否有错误 + if (params.error) { + throw new Error(`Authorization error: ${params.error} - ${params.error_description || ''}`); + } + // 检查是否有授权码 + if (!params.code) { + throw new Error('Authorization code not found'); + } + const registrationId = this.config.registrationId || 'idp'; + const basepath = this.config.basepath || ''; + const callback = `${basepath}/login/oauth2/code/${registrationId}${buildQueryParams(params)}`; + const tokenResponse = await this.httpClient.get(callback, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + needAuth: false + }); + // 触发登录事件 + this.emit('login'); + this.storage.set('userInfo', tokenResponse.data.data); + this.tokenManager.saveToken(tokenResponse.headers['authorization'] || tokenResponse.headers['Authorization']); + let url = this.config.homePage; + if (params.state) { + url = this.storage.get(params.state) || url; + } + window.location.href = url; + } + async getRoutes() { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const basepath = this.config.basepath || ''; + const tokenResponse = await this.httpClient.get(`${basepath}/idp/routes`, { needAuth: true }); + if (tokenResponse.status === 401) { + await this.logout(); + } + return tokenResponse.data.data; + } + /** + * 获取用户信息 + * @returns UserInfo 用户信息 + */ + getUserInfo() { + return this.storage.get("userInfo"); + } + /** + * 检查用户是否有指定角色 + * @param role 角色编码或角色编码列表 + * @returns Promise 是否有指定角色 + */ + async hasRole(role) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const roleCodes = userInfo.roles || []; + if (Array.isArray(role)) { + // 检查是否有任一角色 + return role.some(r => roleCodes.includes(r)); + } + // 检查是否有单个角色 + return roleCodes.includes(role); + } + /** + * 检查用户是否有所有指定角色 + * @param roles 角色编码列表 + * @returns Promise 是否有所有指定角色 + */ + async hasAllRoles(roles) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const roleCodes = userInfo.roles || []; + // 检查是否有所有角色 + return roles.every(r => roleCodes.includes(r)); + } + /** + * 检查用户是否有指定权限 + * @param permission 权限标识或权限标识列表 + * @returns Promise 是否有指定权限 + */ + async hasPermission(permission) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const permissions = userInfo.permissions || []; + if (Array.isArray(permission)) { + // 检查是否有任一权限 + return permission.some(p => permissions.includes(p)); + } + // 检查是否有单个权限 + return permissions.includes(permission); + } + /** + * 检查用户是否有所有指定权限 + * @param permissions 权限标识列表 + * @returns Promise 是否有所有指定权限 + */ + async hasAllPermissions(permissions) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const userPermissions = userInfo.permissions || []; + // 检查是否有所有权限 + return permissions.every(p => userPermissions.includes(p)); + } + /** + * 检查用户是否已认证 + * @returns boolean 是否已认证 + */ + isAuthenticated() { + // 检查Token是否存在且未过期 + return !!this.tokenManager.getToken(); + } + /** + * 事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + on(event, callback) { + this.eventHandlers[event].push(callback); + } + /** + * 移除事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + off(event, callback) { + this.eventHandlers[event] = this.eventHandlers[event].filter(handler => handler !== callback); + } + /** + * 触发事件 + * @param event 事件类型 + * @param data 事件数据 + */ + emit(event, data) { + this.eventHandlers[event].forEach(handler => { + try { + handler(data); + } + catch (error) { + console.error(`Error in ${event} event handler:`, error); + } + }); + } + /** + * 检查当前URL是否为授权回调 + * @returns boolean 是否为授权回调 + */ + isCallback() { + return isCallbackUrl(); + } +} +//# sourceMappingURL=auth.js.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/auth.js.map b/sdk/frontend/oauth2-login-sdk/dist/core/auth.js.map new file mode 100644 index 0000000..f5f40b8 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/auth.js.map @@ -0,0 +1 @@ +{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/http.d.ts b/sdk/frontend/oauth2-login-sdk/dist/core/http.d.ts new file mode 100644 index 0000000..b26f8fa --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/http.d.ts @@ -0,0 +1,155 @@ +/** + * HTTP客户端 + * 用于与后端API进行通信 + */ +/** + * HTTP请求方法类型 + */ +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; +/** + * HTTP请求选项 + */ +export interface HttpRequestOptions { + /** 请求方法 */ + method: HttpMethod; + /** 请求URL */ + url: string; + /** 请求头 */ + headers?: Record; + /** 请求体 */ + body?: any; + /** 是否需要认证 */ + needAuth?: boolean; +} +/** + * HTTP响应类型 + */ +export interface HttpResponse { + /** 状态码 */ + status: number; + /** 状态文本 */ + statusText: string; + /** 响应体 */ + data: T; + /** 响应头 */ + headers: Record; +} +/** + * HTTP错误类型 + */ +export declare class HttpError extends Error { + /** 状态码 */ + status: number; + /** 状态文本 */ + statusText: string; + /** 错误数据 */ + data: any; + /** + * 构造函数 + * @param message 错误信息 + * @param status 状态码 + * @param statusText 状态文本 + * @param data 错误数据 + */ + constructor(message: string, status: number, statusText: string, data: any); +} +/** + * HTTP客户端类 + */ +export declare class HttpClient { + private tokenGetter?; + private tenantId?; + /** + * 构造函数 + * @param logout + * @param tokenGetter Token获取函数 + */ + constructor(tokenGetter?: () => string | null); + /** + * 设置Token获取函数 + * @param tokenGetter Token获取函数 + */ + setTokenGetter(tokenGetter: () => string | null): void; + /** + * 设置租户ID + * @param tenantId 租户ID + */ + setTenantId(tenantId?: string): void; + /** + * 发送HTTP请求 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + request(options: HttpRequestOptions): Promise>; + /** + * 处理响应数据 + * @param response 响应对象 + * @param responseData 响应数据 + * @returns HttpResponse 处理后的响应 + */ + private handleResponse; + /** + * 检查是否为业务响应结构 + * @param responseData 响应数据 + * @returns boolean 是否为业务响应结构 + */ + private isBusinessResponse; + /** + * 获取错误信息 + * @param responseData 响应数据 + * @returns string 错误信息 + */ + private getErrorMessage; + /** + * GET请求 + * @param url 请求URL + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + get(url: string, options?: Omit): Promise>; + /** + * POST请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + post(url: string, body?: any, options?: Omit): Promise>; + /** + * PUT请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + put(url: string, body?: any, options?: Omit): Promise>; + /** + * DELETE请求 + * @param url 请求URL + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + delete(url: string, options?: Omit): Promise>; + /** + * PATCH请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + patch(url: string, body?: any, options?: Omit): Promise>; + /** + * 解析响应体 + * @param response 响应对象 + * @returns Promise 解析后的响应体 + */ + private parseResponse; + /** + * 解析响应头 + * @param headers 响应头对象 + * @returns Record 解析后的响应头 + */ + private parseHeaders; +} +export {}; +//# sourceMappingURL=http.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/http.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/core/http.d.ts.map new file mode 100644 index 0000000..6d65ab1 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/http.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/http.js b/sdk/frontend/oauth2-login-sdk/dist/core/http.js new file mode 100644 index 0000000..edbb21e --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/http.js @@ -0,0 +1,274 @@ +/** + * HTTP客户端 + * 用于与后端API进行通信 + */ +/** + * HTTP错误类型 + */ +export class HttpError extends Error { + /** + * 构造函数 + * @param message 错误信息 + * @param status 状态码 + * @param statusText 状态文本 + * @param data 错误数据 + */ + constructor(message, status, statusText, data) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.statusText = statusText; + this.data = data; + } +} +/** + * HTTP客户端类 + */ +export class HttpClient { + /** + * 构造函数 + * @param logout + * @param tokenGetter Token获取函数 + */ + constructor(tokenGetter) { + this.tokenGetter = tokenGetter; + } + /** + * 设置Token获取函数 + * @param tokenGetter Token获取函数 + */ + setTokenGetter(tokenGetter) { + this.tokenGetter = tokenGetter; + } + /** + * 设置租户ID + * @param tenantId 租户ID + */ + setTenantId(tenantId) { + this.tenantId = tenantId; + } + /** + * 发送HTTP请求 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async request(options) { + const { method, url, headers = {}, body, needAuth = true } = options; + // 构建请求头 + const requestHeaders = { + 'Content-Type': 'application/json', + ...headers + }; + // 添加认证头 + const addAuthHeader = () => { + if (needAuth && this.tokenGetter) { + const token = this.tokenGetter(); + if (token) { + requestHeaders.Authorization = `${token}`; + } + } + }; + // 添加租户ID头 + if (this.tenantId) { + requestHeaders['tenant-id'] = this.tenantId; + } + addAuthHeader(); + // 构建请求配置 + const fetchOptions = { + method, + headers: requestHeaders, + credentials: 'include' // 包含cookie + }; + // 添加请求体 + if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { + fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body); + } + try { + // 发送请求 + const response = await fetch(url, fetchOptions); + const responseData = await this.parseResponse(response); + // 检查响应状态 + if (!response.ok) { + // 如果是401错误,尝试刷新Token并重试 + if (response.status === 401) { + return { + status: response.status, + statusText: response.statusText, + data: '', + headers: this.parseHeaders(response.headers) + }; + } + // 其他错误,直接抛出 + const errorMsg = this.getErrorMessage(responseData); + throw new HttpError(errorMsg, response.status, response.statusText, responseData); + } + // 处理成功响应的业务逻辑 + return this.handleResponse(response, responseData); + } + catch (error) { + if (error instanceof HttpError) { + throw error; + } + // 网络错误或其他错误 + throw new HttpError(error instanceof Error ? error.message : 'Network Error', 0, 'Network Error', null); + } + } + /** + * 处理响应数据 + * @param response 响应对象 + * @param responseData 响应数据 + * @returns HttpResponse 处理后的响应 + */ + handleResponse(response, responseData) { + // 检查是否为业务响应结构 + if (this.isBusinessResponse(responseData)) { + // 业务响应结构:{ code, msg, data } + const { code, msg, data } = responseData; + // 检查业务状态码 + if (code !== 0 && code !== 200 && code !== '0' && code !== '200') { + // 业务错误,抛出HttpError + throw new HttpError(msg || `Business Error: ${code}`, response.status, response.statusText, responseData); + } + // 业务成功,返回data字段作为实际数据 + return { + status: response.status, + statusText: response.statusText, + data: data, + headers: this.parseHeaders(response.headers) + }; + } + // 非业务响应结构,直接返回原始数据 + return { + status: response.status, + statusText: response.statusText, + data: responseData, + headers: this.parseHeaders(response.headers) + }; + } + /** + * 检查是否为业务响应结构 + * @param responseData 响应数据 + * @returns boolean 是否为业务响应结构 + */ + isBusinessResponse(responseData) { + return typeof responseData === 'object' && + responseData !== null && + ('code' in responseData) && + ('msg' in responseData) && + ('data' in responseData); + } + /** + * 获取错误信息 + * @param responseData 响应数据 + * @returns string 错误信息 + */ + getErrorMessage(responseData) { + // 如果是业务响应结构 + if (this.isBusinessResponse(responseData)) { + return responseData.msg || `Business Error: ${responseData.code}`; + } + // 其他错误结构 + return responseData.message || responseData.error || `HTTP Error`; + } + /** + * GET请求 + * @param url 请求URL + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async get(url, options) { + return this.request({ + method: 'GET', + url, + ...options + }); + } + /** + * POST请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async post(url, body, options) { + return this.request({ + method: 'POST', + url, + body, + ...options + }); + } + /** + * PUT请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async put(url, body, options) { + return this.request({ + method: 'PUT', + url, + body, + ...options + }); + } + /** + * DELETE请求 + * @param url 请求URL + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async delete(url, options) { + return this.request({ + method: 'DELETE', + url, + ...options + }); + } + /** + * PATCH请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async patch(url, body, options) { + return this.request({ + method: 'PATCH', + url, + body, + ...options + }); + } + /** + * 解析响应体 + * @param response 响应对象 + * @returns Promise 解析后的响应体 + */ + async parseResponse(response) { + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('application/json')) { + return response.json(); + } + else if (contentType.includes('text/')) { + return response.text(); + } + else { + return response.blob(); + } + } + /** + * 解析响应头 + * @param headers 响应头对象 + * @returns Record 解析后的响应头 + */ + parseHeaders(headers) { + const result = {}; + headers.forEach((value, key) => { + result[key] = value; + }); + return result; + } +} +//# sourceMappingURL=http.js.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/http.js.map b/sdk/frontend/oauth2-login-sdk/dist/core/http.js.map new file mode 100644 index 0000000..a7de670 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/http.js.map @@ -0,0 +1 @@ +{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAqCH;;GAEG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IAQlC;;;;;;OAMG;IACH,YAAY,OAAe,EAAE,MAAc,EAAE,UAAkB,EAAE,IAAS;QACxE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED;;GAEG;AACH,MAAM,OAAO,UAAU;IAIrB;;;;OAIG;IACH,YAAY,WAAiC;QAC3C,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IAEjC,CAAC;IAED;;;OAGG;IACH,cAAc,CAAC,WAAgC;QAC7C,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,QAAiB;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO,CAAU,OAA2B;QAChD,MAAM,EACJ,MAAM,EACN,GAAG,EACH,OAAO,GAAG,EAAE,EACZ,IAAI,EACJ,QAAQ,GAAG,IAAI,EAChB,GAAG,OAAO,CAAC;QAEZ,QAAQ;QACR,MAAM,cAAc,GAA2B;YAC7C,cAAc,EAAE,kBAAkB;YAClC,GAAG,OAAO;SACX,CAAC;QAEF,QAAQ;QACR,MAAM,aAAa,GAAG,GAAG,EAAE;YACzB,IAAI,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjC,IAAI,KAAK,EAAE,CAAC;oBACV,cAAc,CAAC,aAAa,GAAG,GAAG,KAAK,EAAE,CAAC;gBAC5C,CAAC;YACH,CAAC;QACH,CAAC,CAAC;QAEF,UAAU;QACV,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,cAAc,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9C,CAAC;QAED,aAAa,EAAE,CAAC;QAEhB,SAAS;QACT,MAAM,YAAY,GAAgB;YAChC,MAAM;YACN,OAAO,EAAE,cAAc;YACvB,WAAW,EAAE,SAAS,CAAC,WAAW;SACnC,CAAC;QAEF,QAAQ;QACR,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,OAAO,CAAC,EAAE,CAAC;YAC1E,YAAY,CAAC,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC7E,CAAC;QAED,IAAI,CAAC;YACH,OAAO;YACP,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;YAChD,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YAExD,SAAS;YACT,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,wBAAwB;gBACxB,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;oBAC5B,OAAO;wBACL,MAAM,EAAE,QAAQ,CAAC,MAAM;wBACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;wBAC/B,IAAI,EAAE,EAAO;wBACb,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC;qBAC7C,CAAA;gBACH,CAAC;gBAED,YAAY;gBACZ,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;gBACpD,MAAM,IAAI,SAAS,CACjB,QAAQ,EACR,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,UAAU,EACnB,YAAY,CACb,CAAC;YACJ,CAAC;YAED,cAAc;YACd,OAAO,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QACrD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,SAAS,EAAE,CAAC;gBAC/B,MAAM,KAAK,CAAC;YACd,CAAC;YAED,YAAY;YACZ,MAAM,IAAI,SAAS,CACjB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EACxD,CAAC,EACD,eAAe,EACf,IAAI,CACL,CAAC;QACJ,CAAC;IACH,CAAC;IAID;;;;;OAKG;IACK,cAAc,CAAU,QAAkB,EAAE,YAAiB;QACnE,cAAc;QACd,IAAI,IAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,CAAC;YAC1C,6BAA6B;YAC7B,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC;YAEzC,UAAU;YACV,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;gBACjE,mBAAmB;gBACnB,MAAM,IAAI,SAAS,CACjB,GAAG,IAAI,mBAAmB,IAAI,EAAE,EAChC,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,UAAU,EACnB,YAAY,CACb,CAAC;YACJ,CAAC;YAED,sBAAsB;YACtB,OAAO;gBACL,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;gBAC/B,IAAI,EAAE,IAAS;gBACf,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC;aAC7C,CAAC;QACJ,CAAC;QAED,mBAAmB;QACnB,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,IAAI,EAAE,YAAiB;YACvB,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC;SAC7C,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACK,kBAAkB,CAAC,YAAiB;QAC1C,OAAO,OAAO,YAAY,KAAK,QAAQ;YAChC,YAAY,KAAK,IAAI;YACrB,CAAC,MAAM,IAAI,YAAY,CAAC;YACxB,CAAC,KAAK,IAAI,YAAY,CAAC;YACvB,CAAC,MAAM,IAAI,YAAY,CAAC,CAAC;IAClC,CAAC;IAED;;;;OAIG;IACK,eAAe,CAAC,YAAiB;QACvC,YAAY;QACZ,IAAI,IAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,CAAC;YAC1C,OAAO,YAAY,CAAC,GAAG,IAAI,mBAAmB,YAAY,CAAC,IAAI,EAAE,CAAC;QACpE,CAAC;QAED,SAAS;QACT,OAAO,YAAY,CAAC,OAAO,IAAI,YAAY,CAAC,KAAK,IAAI,YAAY,CAAC;IACpE,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,GAAG,CAAU,GAAW,EAAE,OAAoD;QAClF,OAAO,IAAI,CAAC,OAAO,CAAI;YACrB,MAAM,EAAE,KAAK;YACb,GAAG;YACH,GAAG,OAAO;SACX,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,CAAU,GAAW,EAAE,IAAU,EAAE,OAA6D;QACxG,OAAO,IAAI,CAAC,OAAO,CAAI;YACrB,MAAM,EAAE,MAAM;YACd,GAAG;YACH,IAAI;YACJ,GAAG,OAAO;SACX,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,CAAU,GAAW,EAAE,IAAU,EAAE,OAA6D;QACvG,OAAO,IAAI,CAAC,OAAO,CAAI;YACrB,MAAM,EAAE,KAAK;YACb,GAAG;YACH,IAAI;YACJ,GAAG,OAAO;SACX,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,MAAM,CAAU,GAAW,EAAE,OAAoD;QACrF,OAAO,IAAI,CAAC,OAAO,CAAI;YACrB,MAAM,EAAE,QAAQ;YAChB,GAAG;YACH,GAAG,OAAO;SACX,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,CAAU,GAAW,EAAE,IAAU,EAAE,OAA6D;QACzG,OAAO,IAAI,CAAC,OAAO,CAAI;YACrB,MAAM,EAAE,OAAO;YACf,GAAG;YACH,IAAI;YACJ,GAAG,OAAO;SACX,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,aAAa,CAAC,QAAkB;QAC5C,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAE/D,IAAI,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAC7C,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;QACzB,CAAC;aAAM,IAAI,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACzC,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;QACzB,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,YAAY,CAAC,OAAgB;QACnC,MAAM,MAAM,GAA2B,EAAE,CAAC;QAC1C,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC7B,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACtB,CAAC,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC;CACF"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/token.d.ts b/sdk/frontend/oauth2-login-sdk/dist/core/token.d.ts new file mode 100644 index 0000000..f2e0f8d --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/token.d.ts @@ -0,0 +1,32 @@ +/** + * Token管理模块 + * 负责Token的存储、获取、刷新和过期处理 + */ +import { Storage } from '../utils/storage'; +/** + * Token管理类 + */ +export declare class TokenManager { + private storage; + /** + * 构造函数 + * @param storage 存储实例 + * @param httpClient HTTP客户端实例 + */ + constructor(storage: Storage); + /** + * 存储Token信息 + * @param tokenInfo Token信息 + */ + saveToken(tokenInfo: string): void; + /** + * 获取Token信息 + * @returns TokenInfo | null Token信息 + */ + getToken(): string | null; + /** + * 清除Token信息 + */ + clearToken(): void; +} +//# sourceMappingURL=token.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/token.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/core/token.d.ts.map new file mode 100644 index 0000000..96d0739 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/token.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/core/token.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE3C;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAU;IAEzB;;;;OAIG;gBACS,OAAO,EAAE,OAAO;IAI5B;;;OAGG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIlC;;;OAGG;IACH,QAAQ,IAAI,MAAM,GAAG,IAAI;IAIzB;;OAEG;IACH,UAAU,IAAI,IAAI;CAGnB"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/token.js b/sdk/frontend/oauth2-login-sdk/dist/core/token.js new file mode 100644 index 0000000..e245fa5 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/token.js @@ -0,0 +1,38 @@ +/** + * Token管理模块 + * 负责Token的存储、获取、刷新和过期处理 + */ +/** + * Token管理类 + */ +export class TokenManager { + /** + * 构造函数 + * @param storage 存储实例 + * @param httpClient HTTP客户端实例 + */ + constructor(storage) { + this.storage = storage; + } + /** + * 存储Token信息 + * @param tokenInfo Token信息 + */ + saveToken(tokenInfo) { + this.storage.set('token', tokenInfo); + } + /** + * 获取Token信息 + * @returns TokenInfo | null Token信息 + */ + getToken() { + return this.storage.get('token'); + } + /** + * 清除Token信息 + */ + clearToken() { + this.storage.remove('token'); + } +} +//# sourceMappingURL=token.js.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/core/token.js.map b/sdk/frontend/oauth2-login-sdk/dist/core/token.js.map new file mode 100644 index 0000000..ecfba31 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/core/token.js.map @@ -0,0 +1 @@ +{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/core/token.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;GAEG;AACH,MAAM,OAAO,YAAY;IAGvB;;;;OAIG;IACH,YAAY,OAAgB;QAC1B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,SAAiB;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED;;OAEG;IACH,UAAU;QACR,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;CACF"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/guards/router.d.ts b/sdk/frontend/oauth2-login-sdk/dist/guards/router.d.ts new file mode 100644 index 0000000..b8691a1 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/guards/router.d.ts @@ -0,0 +1,55 @@ +/** + * 路由守卫模块 + * 提供基于权限的路由拦截和未登录自动跳转登录页功能 + */ +import { Auth } from '../core/auth'; +/** + * 路由守卫选项 + */ +export interface RouterGuardOptions { + /** + * 是否需要登录 + */ + requiresAuth?: boolean; + /** + * 需要的权限列表 + */ + requiredPermissions?: string[]; + /** + * 登录后重定向的URL + */ + redirectUri?: string; + /** + * 权限不足时重定向的URL + */ + unauthorizedRedirectUri?: string; +} +/** + * 路由守卫类 + */ +export declare class RouterGuard { + private auth; + /** + * 构造函数 + * @param auth 认证实例 + */ + constructor(auth: Auth); + /** + * 检查路由权限 + * @param options 路由守卫选项 + * @returns Promise 是否通过权限检查 + */ + check(options: RouterGuardOptions): Promise; + /** + * 创建Vue路由守卫 + * @returns 路由守卫函数 + */ + createVueGuard(): (to: any, from: any, next: any) => Promise; + /** + * 检查当前用户是否有权限访问资源 + * @param permissions 需要的权限列表 + * @returns Promise 是否拥有权限 + */ + hasPermission(permissions: string | string[]): Promise; +} +//# sourceMappingURL=router.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/guards/router.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/guards/router.d.ts.map new file mode 100644 index 0000000..a6e0a1f --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/guards/router.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/guards/router.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAO;IAEnB;;;OAGG;gBACS,IAAI,EAAE,IAAI;IAItB;;;;OAIG;IACG,KAAK,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAmC1D;;;OAGG;IACH,cAAc,KACE,IAAI,GAAG,EAAE,MAAM,GAAG,EAAE,MAAM,GAAG;IAgB7C;;;;OAIG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAoBtE"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/guards/router.js b/sdk/frontend/oauth2-login-sdk/dist/guards/router.js new file mode 100644 index 0000000..2bcadd6 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/guards/router.js @@ -0,0 +1,89 @@ +/** + * 路由守卫模块 + * 提供基于权限的路由拦截和未登录自动跳转登录页功能 + */ +/** + * 路由守卫类 + */ +export class RouterGuard { + /** + * 构造函数 + * @param auth 认证实例 + */ + constructor(auth) { + this.auth = auth; + } + /** + * 检查路由权限 + * @param options 路由守卫选项 + * @returns Promise 是否通过权限检查 + */ + async check(options) { + const { requiresAuth = true, requiredPermissions = [] } = options; + // 检查是否需要登录 + if (requiresAuth) { + // 检查是否已认证 + if (!this.auth.isAuthenticated()) { + // 未认证,跳转到登录页 + this.auth.login(options.redirectUri); + return false; + } + // 检查是否需要权限 + if (requiredPermissions.length > 0) { + // 获取用户权限 + const userPermissions = ['']; + // 检查是否拥有所有需要的权限 + const hasPermission = requiredPermissions.every(permission => userPermissions.includes(permission)); + if (!hasPermission) { + // 权限不足,跳转到权限不足页 + if (options.unauthorizedRedirectUri) { + window.location.href = options.unauthorizedRedirectUri; + } + return false; + } + } + } + return true; + } + /** + * 创建Vue路由守卫 + * @returns 路由守卫函数 + */ + createVueGuard() { + return async (to, from, next) => { + var _a; + // 从路由元信息中获取守卫选项 + const options = ((_a = to.meta) === null || _a === void 0 ? void 0 : _a.auth) || {}; + try { + const allowed = await this.check(options); + if (allowed) { + next(); + } + } + catch (error) { + console.error('Route guard error:', error); + next(false); + } + }; + } + /** + * 检查当前用户是否有权限访问资源 + * @param permissions 需要的权限列表 + * @returns Promise 是否拥有权限 + */ + async hasPermission(permissions) { + if (!permissions) { + return true; + } + const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions]; + // 检查是否已认证 + if (!this.auth.isAuthenticated()) { + return false; + } + // 获取用户权限 + const userPermissions = ['']; + // 检查是否拥有所有需要的权限 + return requiredPermissions.every(permission => userPermissions.includes(permission)); + } +} +//# sourceMappingURL=router.js.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/guards/router.js.map b/sdk/frontend/oauth2-login-sdk/dist/guards/router.js.map new file mode 100644 index 0000000..fc72a2e --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/guards/router.js.map @@ -0,0 +1 @@ +{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/guards/router.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA0BH;;GAEG;AACH,MAAM,OAAO,WAAW;IAGtB;;;OAGG;IACH,YAAY,IAAU;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK,CAAC,OAA2B;QACrC,MAAM,EAAE,YAAY,GAAG,IAAI,EAAE,mBAAmB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;QAElE,WAAW;QACX,IAAI,YAAY,EAAE,CAAC;YACjB,UAAU;YACV,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;gBACjC,aAAa;gBACb,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;gBACrC,OAAO,KAAK,CAAC;YACf,CAAC;YAED,WAAW;YACX,IAAI,mBAAmB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACnC,SAAS;gBACT,MAAM,eAAe,GAAG,CAAC,EAAE,CAAC,CAAC;gBAE7B,gBAAgB;gBAChB,MAAM,aAAa,GAAG,mBAAmB,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAC3D,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC,CACrC,CAAC;gBAEF,IAAI,CAAC,aAAa,EAAE,CAAC;oBACnB,gBAAgB;oBAChB,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC;wBACpC,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,uBAAuB,CAAC;oBACzD,CAAC;oBACD,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,cAAc;QACZ,OAAO,KAAK,EAAE,EAAO,EAAE,IAAS,EAAE,IAAS,EAAE,EAAE;;YAC7C,gBAAgB;YAChB,MAAM,OAAO,GAAuB,CAAA,MAAA,EAAE,CAAC,IAAI,0CAAE,IAAI,KAAI,EAAE,CAAC;YAExD,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC1C,IAAI,OAAO,EAAE,CAAC;oBACZ,IAAI,EAAE,CAAC;gBACT,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAC;gBAC3C,IAAI,CAAC,KAAK,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,aAAa,CAAC,WAA8B;QAChD,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,mBAAmB,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;QAErF,UAAU;QACV,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC;QACf,CAAC;QAED,SAAS;QACT,MAAM,eAAe,GAAG,CAAC,EAAE,CAAC,CAAA;QAE5B,gBAAgB;QAChB,OAAO,mBAAmB,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAC5C,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC,CACrC,CAAC;IACJ,CAAC;CACF"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/index.d.ts b/sdk/frontend/oauth2-login-sdk/dist/index.d.ts new file mode 100644 index 0000000..c860804 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/index.d.ts @@ -0,0 +1,20 @@ +/** + * 统一登录SDK入口文件 + * 支持OAuth2授权码模式,提供完整的Token管理和用户信息管理功能 + */ +export { Auth } from './core/auth'; +export { TokenManager } from './core/token'; +export { HttpClient, HttpError } from './core/http'; +export { Storage } from './utils/storage'; +export { RouterGuard, RouterGuardOptions } from './guards/router'; +export { generateRandomString, parseQueryParams, buildQueryParams, generateAuthorizationUrl, isCallbackUrl } from './utils/url'; +export * from './types'; +export { VuePlugin, createVuePlugin } from './plugins/vue'; +import { UnifiedLoginSDK } from './types'; +/** + * 默认导出的SDK实例 + */ +export declare const unifiedLoginSDK: UnifiedLoginSDK; +export default unifiedLoginSDK; +export declare const version = "1.0.0"; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/index.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/index.d.ts.map new file mode 100644 index 0000000..5794fd5 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,wBAAwB,EACxB,aAAa,EACd,MAAM,aAAa,CAAC;AAGrB,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,EAAa,eAAe,EAAE,MAAM,SAAS,CAAC;AAUrD;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,eA8C7B,CAAC;AAGF,eAAe,eAAe,CAAC;AAG/B,eAAO,MAAM,OAAO,UAAU,CAAC"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/index.esm.js b/sdk/frontend/oauth2-login-sdk/dist/index.esm.js new file mode 100644 index 0000000..d2ec6d2 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/index.esm.js @@ -0,0 +1,1192 @@ +/** + * Token管理模块 + * 负责Token的存储、获取、刷新和过期处理 + */ +/** + * Token管理类 + */ +class TokenManager { + /** + * 构造函数 + * @param storage 存储实例 + * @param httpClient HTTP客户端实例 + */ + constructor(storage) { + this.storage = storage; + } + /** + * 存储Token信息 + * @param tokenInfo Token信息 + */ + saveToken(tokenInfo) { + this.storage.set('token', tokenInfo); + } + /** + * 获取Token信息 + * @returns TokenInfo | null Token信息 + */ + getToken() { + return this.storage.get('token'); + } + /** + * 清除Token信息 + */ + clearToken() { + this.storage.remove('token'); + } +} + +/** + * HTTP客户端 + * 用于与后端API进行通信 + */ +/** + * HTTP错误类型 + */ +class HttpError extends Error { + /** + * 构造函数 + * @param message 错误信息 + * @param status 状态码 + * @param statusText 状态文本 + * @param data 错误数据 + */ + constructor(message, status, statusText, data) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.statusText = statusText; + this.data = data; + } +} +/** + * HTTP客户端类 + */ +class HttpClient { + /** + * 构造函数 + * @param logout + * @param tokenGetter Token获取函数 + */ + constructor(tokenGetter) { + this.tokenGetter = tokenGetter; + } + /** + * 设置Token获取函数 + * @param tokenGetter Token获取函数 + */ + setTokenGetter(tokenGetter) { + this.tokenGetter = tokenGetter; + } + /** + * 设置租户ID + * @param tenantId 租户ID + */ + setTenantId(tenantId) { + this.tenantId = tenantId; + } + /** + * 发送HTTP请求 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async request(options) { + const { method, url, headers = {}, body, needAuth = true } = options; + // 构建请求头 + const requestHeaders = { + 'Content-Type': 'application/json', + ...headers + }; + // 添加认证头 + const addAuthHeader = () => { + if (needAuth && this.tokenGetter) { + const token = this.tokenGetter(); + if (token) { + requestHeaders.Authorization = `${token}`; + } + } + }; + // 添加租户ID头 + if (this.tenantId) { + requestHeaders['tenant-id'] = this.tenantId; + } + addAuthHeader(); + // 构建请求配置 + const fetchOptions = { + method, + headers: requestHeaders, + credentials: 'include' // 包含cookie + }; + // 添加请求体 + if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { + fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body); + } + try { + // 发送请求 + const response = await fetch(url, fetchOptions); + const responseData = await this.parseResponse(response); + // 检查响应状态 + if (!response.ok) { + // 如果是401错误,尝试刷新Token并重试 + if (response.status === 401) { + return { + status: response.status, + statusText: response.statusText, + data: '', + headers: this.parseHeaders(response.headers) + }; + } + // 其他错误,直接抛出 + const errorMsg = this.getErrorMessage(responseData); + throw new HttpError(errorMsg, response.status, response.statusText, responseData); + } + // 处理成功响应的业务逻辑 + return this.handleResponse(response, responseData); + } + catch (error) { + if (error instanceof HttpError) { + throw error; + } + // 网络错误或其他错误 + throw new HttpError(error instanceof Error ? error.message : 'Network Error', 0, 'Network Error', null); + } + } + /** + * 处理响应数据 + * @param response 响应对象 + * @param responseData 响应数据 + * @returns HttpResponse 处理后的响应 + */ + handleResponse(response, responseData) { + // 检查是否为业务响应结构 + if (this.isBusinessResponse(responseData)) { + // 业务响应结构:{ code, msg, data } + const { code, msg, data } = responseData; + // 检查业务状态码 + if (code !== 0 && code !== 200 && code !== '0' && code !== '200') { + // 业务错误,抛出HttpError + throw new HttpError(msg || `Business Error: ${code}`, response.status, response.statusText, responseData); + } + // 业务成功,返回data字段作为实际数据 + return { + status: response.status, + statusText: response.statusText, + data: data, + headers: this.parseHeaders(response.headers) + }; + } + // 非业务响应结构,直接返回原始数据 + return { + status: response.status, + statusText: response.statusText, + data: responseData, + headers: this.parseHeaders(response.headers) + }; + } + /** + * 检查是否为业务响应结构 + * @param responseData 响应数据 + * @returns boolean 是否为业务响应结构 + */ + isBusinessResponse(responseData) { + return typeof responseData === 'object' && + responseData !== null && + ('code' in responseData) && + ('msg' in responseData) && + ('data' in responseData); + } + /** + * 获取错误信息 + * @param responseData 响应数据 + * @returns string 错误信息 + */ + getErrorMessage(responseData) { + // 如果是业务响应结构 + if (this.isBusinessResponse(responseData)) { + return responseData.msg || `Business Error: ${responseData.code}`; + } + // 其他错误结构 + return responseData.message || responseData.error || `HTTP Error`; + } + /** + * GET请求 + * @param url 请求URL + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async get(url, options) { + return this.request({ + method: 'GET', + url, + ...options + }); + } + /** + * POST请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async post(url, body, options) { + return this.request({ + method: 'POST', + url, + body, + ...options + }); + } + /** + * PUT请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async put(url, body, options) { + return this.request({ + method: 'PUT', + url, + body, + ...options + }); + } + /** + * DELETE请求 + * @param url 请求URL + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async delete(url, options) { + return this.request({ + method: 'DELETE', + url, + ...options + }); + } + /** + * PATCH请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async patch(url, body, options) { + return this.request({ + method: 'PATCH', + url, + body, + ...options + }); + } + /** + * 解析响应体 + * @param response 响应对象 + * @returns Promise 解析后的响应体 + */ + async parseResponse(response) { + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('application/json')) { + return response.json(); + } + else if (contentType.includes('text/')) { + return response.text(); + } + else { + return response.blob(); + } + } + /** + * 解析响应头 + * @param headers 响应头对象 + * @returns Record 解析后的响应头 + */ + parseHeaders(headers) { + const result = {}; + headers.forEach((value, key) => { + result[key] = value; + }); + return result; + } +} + +/** + * URL处理工具 + * 用于生成授权URL、解析URL参数等功能 + */ +/** + * 生成随机字符串 + * @param length 字符串长度,默认32位 + * @returns 随机字符串 + */ +function generateRandomString(length = 32) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} +/** + * 解析URL查询参数 + * @param url URL字符串,默认为当前URL + * @returns 查询参数对象 + */ +function parseQueryParams(url = window.location.href) { + const params = {}; + const queryString = url.split('?')[1]; + if (!queryString) { + return params; + } + const pairs = queryString.split('&'); + for (const pair of pairs) { + const [key, value] = pair.split('='); + if (key) { + params[decodeURIComponent(key)] = decodeURIComponent(value || ''); + } + } + return params; +} +/** + * 构建URL查询参数 + * @param params 查询参数对象 + * @returns 查询参数字符串 + */ +function buildQueryParams(params) { + const pairs = []; + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); + } + } + return pairs.length ? `?${pairs.join('&')}` : ''; +} +/** + * 生成OAuth2授权URL + * @param authorizationEndpoint 授权端点URL + * @param clientId 客户端ID + * @param redirectUri 重定向URL + * @param options 可选参数 + * @returns 授权URL + */ +function generateAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, options) { + const { responseType = 'code', scope, state = generateRandomString(32), ...extraParams } = options || {}; + const params = { + client_id: clientId, + redirect_uri: redirectUri, + response_type: responseType, + state, + ...(scope ? { scope } : {}), + ...extraParams + }; + const queryString = buildQueryParams(params); + return `${authorizationEndpoint}${queryString}`; +} +/** + * 检查当前URL是否为授权回调 + * @param url URL字符串,默认为当前URL + * @returns 是否为授权回调 + */ +function isCallbackUrl(url = window.location.href) { + const params = parseQueryParams(url); + return !!params.code || !!params.error; +} + +/** + * 认证核心逻辑 + * 实现OAuth2授权码模式的完整流程 + */ +/** + * 认证核心类 + */ +class Auth { + /** + * 构造函数 + * @param storage 存储实例 + */ + constructor(storage) { + this.config = null; + this.eventHandlers = { + login: [], + logout: [], + tokenExpired: [] + }; + this.userInfoCache = null; + this.storage = storage; + // 先创建HttpClient,初始时tokenManager为undefined + this.httpClient = new HttpClient(() => this.tokenManager.getToken() || null); + // 然后创建TokenManager + this.tokenManager = new TokenManager(storage); + } + /** + * 初始化SDK配置 + * @param config SDK配置选项 + */ + init(config) { + this.config = config; + // 设置租户ID到HTTP客户端 + this.httpClient.setTenantId(config.tenantId); + } + getToken() { + return this.tokenManager.getToken(); + } + /** + * 触发登录流程 + * @param redirectUri 可选的重定向URL,覆盖初始化时的配置 + */ + async login(redirectUri) { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const registrationId = this.config.registrationId || 'idp'; + const basepath = this.config.basepath || ''; + const path = `${basepath}/oauth2/authorization/${registrationId}`; + const tokenResponse = await this.httpClient.get(path, { needAuth: false }); + const redirect = tokenResponse.data.redirect_url; + const params = parseQueryParams(redirect); + this.storage.set(params.state, window.location.href); + window.location.href = redirect; + } + /** + * 退出登录 + */ + async logout() { + if (!this.config) { + throw new Error('SDK not initialized'); + } + // 清除本地存储的Token和用户信息 + this.tokenManager.clearToken(); + this.userInfoCache = null; + this.storage.remove('userInfo'); + const basepath = this.config.basepath || ''; + await this.httpClient.post(`${basepath}/logout`, null, { needAuth: true }); + // 触发退出事件 + this.emit('logout'); + window.location.href = this.config.idpLogoutUrl + '?redirect=' + this.config.homePage; + } + /** + * 处理授权回调 + * @returns Promise 用户信息 + */ + async handleCallback() { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const params = parseQueryParams(); + // 检查是否有错误 + if (params.error) { + throw new Error(`Authorization error: ${params.error} - ${params.error_description || ''}`); + } + // 检查是否有授权码 + if (!params.code) { + throw new Error('Authorization code not found'); + } + const registrationId = this.config.registrationId || 'idp'; + const basepath = this.config.basepath || ''; + const callback = `${basepath}/login/oauth2/code/${registrationId}${buildQueryParams(params)}`; + const tokenResponse = await this.httpClient.get(callback, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + needAuth: false + }); + // 触发登录事件 + this.emit('login'); + this.storage.set('userInfo', tokenResponse.data.data); + this.tokenManager.saveToken(tokenResponse.headers['authorization'] || tokenResponse.headers['Authorization']); + let url = this.config.homePage; + if (params.state) { + url = this.storage.get(params.state) || url; + } + window.location.href = url; + } + async getRoutes() { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const basepath = this.config.basepath || ''; + const tokenResponse = await this.httpClient.get(`${basepath}/idp/routes`, { needAuth: true }); + if (tokenResponse.status === 401) { + await this.logout(); + } + return tokenResponse.data.data; + } + /** + * 获取用户信息 + * @returns UserInfo 用户信息 + */ + getUserInfo() { + return this.storage.get("userInfo"); + } + /** + * 检查用户是否有指定角色 + * @param role 角色编码或角色编码列表 + * @returns Promise 是否有指定角色 + */ + async hasRole(role) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const roleCodes = userInfo.roles || []; + if (Array.isArray(role)) { + // 检查是否有任一角色 + return role.some(r => roleCodes.includes(r)); + } + // 检查是否有单个角色 + return roleCodes.includes(role); + } + /** + * 检查用户是否有所有指定角色 + * @param roles 角色编码列表 + * @returns Promise 是否有所有指定角色 + */ + async hasAllRoles(roles) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const roleCodes = userInfo.roles || []; + // 检查是否有所有角色 + return roles.every(r => roleCodes.includes(r)); + } + /** + * 检查用户是否有指定权限 + * @param permission 权限标识或权限标识列表 + * @returns Promise 是否有指定权限 + */ + async hasPermission(permission) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const permissions = userInfo.permissions || []; + if (Array.isArray(permission)) { + // 检查是否有任一权限 + return permission.some(p => permissions.includes(p)); + } + // 检查是否有单个权限 + return permissions.includes(permission); + } + /** + * 检查用户是否有所有指定权限 + * @param permissions 权限标识列表 + * @returns Promise 是否有所有指定权限 + */ + async hasAllPermissions(permissions) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const userPermissions = userInfo.permissions || []; + // 检查是否有所有权限 + return permissions.every(p => userPermissions.includes(p)); + } + /** + * 检查用户是否已认证 + * @returns boolean 是否已认证 + */ + isAuthenticated() { + // 检查Token是否存在且未过期 + return !!this.tokenManager.getToken(); + } + /** + * 事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + on(event, callback) { + this.eventHandlers[event].push(callback); + } + /** + * 移除事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + off(event, callback) { + this.eventHandlers[event] = this.eventHandlers[event].filter(handler => handler !== callback); + } + /** + * 触发事件 + * @param event 事件类型 + * @param data 事件数据 + */ + emit(event, data) { + this.eventHandlers[event].forEach(handler => { + try { + handler(data); + } + catch (error) { + console.error(`Error in ${event} event handler:`, error); + } + }); + } + /** + * 检查当前URL是否为授权回调 + * @returns boolean 是否为授权回调 + */ + isCallback() { + return isCallbackUrl(); + } +} + +/** + * 存储工具类 + * 支持localStorage、sessionStorage和cookie三种存储方式 + */ +/** + * 存储工具类 + */ +class Storage { + /** + * 构造函数 + * @param storageType 存储类型 + * @param prefix 存储前缀,默认'unified_login_' + */ + constructor(storageType = 'localStorage', prefix = 'unified_login_') { + this.storageType = storageType; + this.prefix = prefix; + } + /** + * 设置存储项 + * @param key 存储键 + * @param value 存储值 + * @param options 可选参数,cookie存储时使用 + */ + set(key, value, options) { + const fullKey = this.prefix + key; + const stringValue = typeof value === 'string' ? value : JSON.stringify(value); + switch (this.storageType) { + case 'localStorage': + this.setLocalStorage(fullKey, stringValue); + break; + case 'sessionStorage': + this.setSessionStorage(fullKey, stringValue); + break; + case 'cookie': + this.setCookie(fullKey, stringValue, options); + break; + } + } + /** + * 获取存储项 + * @param key 存储键 + * @returns 存储值 + */ + get(key) { + const fullKey = this.prefix + key; + let value; + switch (this.storageType) { + case 'localStorage': + value = this.getLocalStorage(fullKey); + break; + case 'sessionStorage': + value = this.getSessionStorage(fullKey); + break; + case 'cookie': + value = this.getCookie(fullKey); + break; + default: + value = null; + } + if (value === null) { + return null; + } + // 尝试解析JSON + try { + return JSON.parse(value); + } + catch (e) { + // 如果不是JSON,直接返回字符串 + return value; + } + } + /** + * 移除存储项 + * @param key 存储键 + */ + remove(key) { + const fullKey = this.prefix + key; + switch (this.storageType) { + case 'localStorage': + this.removeLocalStorage(fullKey); + break; + case 'sessionStorage': + this.removeSessionStorage(fullKey); + break; + case 'cookie': + this.removeCookie(fullKey); + break; + } + } + /** + * 清空所有存储项 + */ + clear() { + switch (this.storageType) { + case 'localStorage': + this.clearLocalStorage(); + break; + case 'sessionStorage': + this.clearSessionStorage(); + break; + case 'cookie': + this.clearCookie(); + break; + } + } + /** + * 检查存储类型是否可用 + * @returns boolean 是否可用 + */ + isAvailable() { + try { + switch (this.storageType) { + case 'localStorage': + return this.isLocalStorageAvailable(); + case 'sessionStorage': + return this.isSessionStorageAvailable(); + case 'cookie': + return typeof document !== 'undefined'; + default: + return false; + } + } + catch (e) { + return false; + } + } + // ------------------------ localStorage 操作 ------------------------ + /** + * 设置localStorage + */ + setLocalStorage(key, value) { + if (this.isLocalStorageAvailable()) { + localStorage.setItem(key, value); + } + } + /** + * 获取localStorage + */ + getLocalStorage(key) { + if (this.isLocalStorageAvailable()) { + return localStorage.getItem(key); + } + return null; + } + /** + * 移除localStorage + */ + removeLocalStorage(key) { + if (this.isLocalStorageAvailable()) { + localStorage.removeItem(key); + } + } + /** + * 清空localStorage中所有带前缀的项 + */ + clearLocalStorage() { + if (this.isLocalStorageAvailable()) { + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(this.prefix)) { + localStorage.removeItem(key); + i--; // 索引调整 + } + } + } + } + /** + * 检查localStorage是否可用 + */ + isLocalStorageAvailable() { + if (typeof localStorage === 'undefined') { + return false; + } + try { + const testKey = '__storage_test__'; + localStorage.setItem(testKey, testKey); + localStorage.removeItem(testKey); + return true; + } + catch (e) { + return false; + } + } + // ------------------------ sessionStorage 操作 ------------------------ + /** + * 设置sessionStorage + */ + setSessionStorage(key, value) { + if (this.isSessionStorageAvailable()) { + sessionStorage.setItem(key, value); + } + } + /** + * 获取sessionStorage + */ + getSessionStorage(key) { + if (this.isSessionStorageAvailable()) { + return sessionStorage.getItem(key); + } + return null; + } + /** + * 移除sessionStorage + */ + removeSessionStorage(key) { + if (this.isSessionStorageAvailable()) { + sessionStorage.removeItem(key); + } + } + /** + * 清空sessionStorage中所有带前缀的项 + */ + clearSessionStorage() { + if (this.isSessionStorageAvailable()) { + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + if (key && key.startsWith(this.prefix)) { + sessionStorage.removeItem(key); + i--; // 索引调整 + } + } + } + } + /** + * 检查sessionStorage是否可用 + */ + isSessionStorageAvailable() { + if (typeof sessionStorage === 'undefined') { + return false; + } + try { + const testKey = '__storage_test__'; + sessionStorage.setItem(testKey, testKey); + sessionStorage.removeItem(testKey); + return true; + } + catch (e) { + return false; + } + } + // ------------------------ cookie 操作 ------------------------ + /** + * 设置cookie + */ + setCookie(key, value, options) { + if (typeof document === 'undefined') { + return; + } + let cookieString = `${key}=${encodeURIComponent(value)}`; + if (options) { + // 设置过期时间(秒) + if (options.expires) { + const date = new Date(); + date.setTime(date.getTime() + options.expires * 1000); + cookieString += `; expires=${date.toUTCString()}`; + } + // 设置路径 + if (options.path) { + cookieString += `; path=${options.path}`; + } + // 设置域名 + if (options.domain) { + cookieString += `; domain=${options.domain}`; + } + // 设置secure + if (options.secure) { + cookieString += '; secure'; + } + } + document.cookie = cookieString; + } + /** + * 获取cookie + */ + getCookie(key) { + if (typeof document === 'undefined') { + return null; + } + const name = `${key}=`; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return null; + } + /** + * 移除cookie + */ + removeCookie(key) { + this.setCookie(key, '', { expires: -1 }); + } + /** + * 清空所有带前缀的cookie + */ + clearCookie() { + if (typeof document === 'undefined') { + return; + } + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const eqPos = cookie.indexOf('='); + const key = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + if (key.startsWith(this.prefix)) { + this.removeCookie(key); + } + } + } +} + +/** + * 路由守卫模块 + * 提供基于权限的路由拦截和未登录自动跳转登录页功能 + */ +/** + * 路由守卫类 + */ +class RouterGuard { + /** + * 构造函数 + * @param auth 认证实例 + */ + constructor(auth) { + this.auth = auth; + } + /** + * 检查路由权限 + * @param options 路由守卫选项 + * @returns Promise 是否通过权限检查 + */ + async check(options) { + const { requiresAuth = true, requiredPermissions = [] } = options; + // 检查是否需要登录 + if (requiresAuth) { + // 检查是否已认证 + if (!this.auth.isAuthenticated()) { + // 未认证,跳转到登录页 + this.auth.login(options.redirectUri); + return false; + } + // 检查是否需要权限 + if (requiredPermissions.length > 0) { + // 获取用户权限 + const userPermissions = ['']; + // 检查是否拥有所有需要的权限 + const hasPermission = requiredPermissions.every(permission => userPermissions.includes(permission)); + if (!hasPermission) { + // 权限不足,跳转到权限不足页 + if (options.unauthorizedRedirectUri) { + window.location.href = options.unauthorizedRedirectUri; + } + return false; + } + } + } + return true; + } + /** + * 创建Vue路由守卫 + * @returns 路由守卫函数 + */ + createVueGuard() { + return async (to, from, next) => { + var _a; + // 从路由元信息中获取守卫选项 + const options = ((_a = to.meta) === null || _a === void 0 ? void 0 : _a.auth) || {}; + try { + const allowed = await this.check(options); + if (allowed) { + next(); + } + } + catch (error) { + console.error('Route guard error:', error); + next(false); + } + }; + } + /** + * 检查当前用户是否有权限访问资源 + * @param permissions 需要的权限列表 + * @returns Promise 是否拥有权限 + */ + async hasPermission(permissions) { + if (!permissions) { + return true; + } + const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions]; + // 检查是否已认证 + if (!this.auth.isAuthenticated()) { + return false; + } + // 获取用户权限 + const userPermissions = ['']; + // 检查是否拥有所有需要的权限 + return requiredPermissions.every(permission => userPermissions.includes(permission)); + } +} + +/** + * Vue插件模块 + * 提供Vue应用中使用统一登录SDK的能力 + */ +/** + * Vue插件类 + */ +class VuePlugin { + /** + * 构造函数 + * @param storage 存储实例 + */ + constructor(storage) { + this.auth = new Auth(storage); + this.routerGuard = new RouterGuard(this.auth); + } + /** + * 安装Vue插件 + * @param app Vue构造函数或Vue 3应用实例 + * @param options 插件选项 + */ + install(app, options) { + const { config, pluginName = 'unifiedLogin' } = options; + // 初始化SDK + this.auth.init(config); + // 判断是Vue 2还是Vue 3 + const isVue3 = typeof app.config !== 'undefined'; + if (isVue3) { + // Vue 3 + // 在全局属性上挂载SDK实例 + app.config.globalProperties[`${pluginName}`] = this.auth; + app.config.globalProperties.$auth = this.auth; // 兼容简写 + // 提供Vue组件内的注入 + app.provide(pluginName, this.auth); + app.provide('auth', this.auth); // 兼容简写 + // 处理路由守卫 + app.mixin({ + beforeCreate() { + // 如果是根组件,添加路由守卫 + if (this.$options.router) { + const router = this.$options.router; + // 添加全局前置守卫 + router.beforeEach(this.routerGuard.createVueGuard()); + } + } + }); + } + else { + // Vue 2 + // 在Vue实例上挂载SDK实例 + app.prototype[`${pluginName}`] = this.auth; + app.prototype.$auth = this.auth; // 兼容简写 + // 全局混入 + app.mixin({ + beforeCreate() { + // 如果是根组件,添加路由守卫 + if (this.$options.router) { + const router = this.$options.router; + // 添加全局前置守卫 + router.beforeEach(this.routerGuard.createVueGuard()); + } + } + }); + } + } + /** + * 获取认证实例 + * @returns Auth 认证实例 + */ + getAuth() { + return this.auth; + } + /** + * 获取路由守卫实例 + * @returns RouterGuard 路由守卫实例 + */ + getRouterGuard() { + return this.routerGuard; + } +} +/** + * 创建Vue插件实例 + * @param storageType 存储类型 + * @returns VuePlugin Vue插件实例 + */ +function createVuePlugin(storageType) { + const storage = new Storage(storageType); + return new VuePlugin(storage); +} + +/** + * 统一登录SDK入口文件 + * 支持OAuth2授权码模式,提供完整的Token管理和用户信息管理功能 + */ +// 导出核心类和功能 +/** + * 默认SDK实例 + */ +const defaultStorage = new Storage(); +const defaultAuth = new Auth(defaultStorage); +/** + * 默认导出的SDK实例 + */ +const unifiedLoginSDK = { + init: (config) => { + defaultAuth.init(config); + }, + getToken: () => { + return defaultAuth.getToken(); + }, + login: (redirectUri) => { + return defaultAuth.login(redirectUri); + }, + logout: () => { + return defaultAuth.logout(); + }, + handleCallback: () => { + return defaultAuth.handleCallback(); + }, + getRoutes: () => { + return defaultAuth.getRoutes(); + }, + getUserInfo: () => { + return defaultAuth.getUserInfo(); + }, + isAuthenticated: () => { + return defaultAuth.isAuthenticated(); + }, + hasRole: (role) => { + return defaultAuth.hasRole(role); + }, + hasAllRoles: (roles) => { + return defaultAuth.hasAllRoles(roles); + }, + hasPermission: (permission) => { + return defaultAuth.hasPermission(permission); + }, + hasAllPermissions: (permissions) => { + return defaultAuth.hasAllPermissions(permissions); + }, + on: (event, callback) => { + return defaultAuth.on(event, callback); + }, + off: (event, callback) => { + return defaultAuth.off(event, callback); + }, + isCallback: () => { + return defaultAuth.isCallback(); + } +}; +// 版本信息 +const version = '1.0.0'; + +export { Auth, HttpClient, HttpError, RouterGuard, Storage, TokenManager, VuePlugin, buildQueryParams, createVuePlugin, unifiedLoginSDK as default, generateAuthorizationUrl, generateRandomString, isCallbackUrl, parseQueryParams, unifiedLoginSDK, version }; +//# sourceMappingURL=index.esm.js.map diff --git a/sdk/frontend/oauth2-login-sdk/dist/index.esm.js.map b/sdk/frontend/oauth2-login-sdk/dist/index.esm.js.map new file mode 100644 index 0000000..14968ca --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/index.esm.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.esm.js","sources":["../src/core/token.ts","../src/core/http.ts","../src/utils/url.ts","../src/core/auth.ts","../src/utils/storage.ts","../src/guards/router.ts","../src/plugins/vue.ts","../src/index.ts"],"sourcesContent":["/**\r\n * Token管理模块\r\n * 负责Token的存储、获取、刷新和过期处理\r\n */\r\n\r\nimport { Storage } from '../utils/storage';\r\n\r\n/**\r\n * Token管理类\r\n */\r\nexport class TokenManager {\r\n private storage: Storage;\r\n\r\n /**\r\n * 构造函数\r\n * @param storage 存储实例\r\n * @param httpClient HTTP客户端实例\r\n */\r\n constructor(storage: Storage) {\r\n this.storage = storage;\r\n }\r\n\r\n /**\r\n * 存储Token信息\r\n * @param tokenInfo Token信息\r\n */\r\n saveToken(tokenInfo: string): void {\r\n this.storage.set('token', tokenInfo);\r\n }\r\n\r\n /**\r\n * 获取Token信息\r\n * @returns TokenInfo | null Token信息\r\n */\r\n getToken(): string | null {\r\n return this.storage.get('token');\r\n }\r\n\r\n /**\r\n * 清除Token信息\r\n */\r\n clearToken(): void {\r\n this.storage.remove('token');\r\n }\r\n}\r\n","/**\r\n * HTTP客户端\r\n * 用于与后端API进行通信\r\n */\r\n\r\n/**\r\n * HTTP请求方法类型\r\n */\r\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\r\n\r\n/**\r\n * HTTP请求选项\r\n */\r\nexport interface HttpRequestOptions {\r\n /** 请求方法 */\r\n method: HttpMethod;\r\n /** 请求URL */\r\n url: string;\r\n /** 请求头 */\r\n headers?: Record;\r\n /** 请求体 */\r\n body?: any;\r\n /** 是否需要认证 */\r\n needAuth?: boolean;\r\n}\r\n\r\n/**\r\n * HTTP响应类型\r\n */\r\nexport interface HttpResponse {\r\n /** 状态码 */\r\n status: number;\r\n /** 状态文本 */\r\n statusText: string;\r\n /** 响应体 */\r\n data: T;\r\n /** 响应头 */\r\n headers: Record;\r\n}\r\n\r\n/**\r\n * HTTP错误类型\r\n */\r\nexport class HttpError extends Error {\r\n /** 状态码 */\r\n public status: number;\r\n /** 状态文本 */\r\n public statusText: string;\r\n /** 错误数据 */\r\n public data: any;\r\n\r\n /**\r\n * 构造函数\r\n * @param message 错误信息\r\n * @param status 状态码\r\n * @param statusText 状态文本\r\n * @param data 错误数据\r\n */\r\n constructor(message: string, status: number, statusText: string, data: any) {\r\n super(message);\r\n this.name = 'HttpError';\r\n this.status = status;\r\n this.statusText = statusText;\r\n this.data = data;\r\n }\r\n}\r\n\r\n/**\r\n * HTTP客户端类\r\n */\r\nexport class HttpClient {\r\n private tokenGetter?: () => string | null;\r\n private tenantId?: string;\r\n\r\n /**\r\n * 构造函数\r\n * @param logout\r\n * @param tokenGetter Token获取函数\r\n */\r\n constructor(tokenGetter?: () => string | null) {\r\n this.tokenGetter = tokenGetter;\r\n\r\n }\r\n\r\n /**\r\n * 设置Token获取函数\r\n * @param tokenGetter Token获取函数\r\n */\r\n setTokenGetter(tokenGetter: () => string | null): void {\r\n this.tokenGetter = tokenGetter;\r\n }\r\n\r\n /**\r\n * 设置租户ID\r\n * @param tenantId 租户ID\r\n */\r\n setTenantId(tenantId?: string): void {\r\n this.tenantId = tenantId;\r\n }\r\n\r\n /**\r\n * 发送HTTP请求\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async request(options: HttpRequestOptions): Promise> {\r\n const {\r\n method,\r\n url,\r\n headers = {},\r\n body,\r\n needAuth = true\r\n } = options;\r\n\r\n // 构建请求头\r\n const requestHeaders: Record = {\r\n 'Content-Type': 'application/json',\r\n ...headers\r\n };\r\n\r\n // 添加认证头\r\n const addAuthHeader = () => {\r\n if (needAuth && this.tokenGetter) {\r\n const token = this.tokenGetter();\r\n if (token) {\r\n requestHeaders.Authorization = `${token}`;\r\n }\r\n }\r\n };\r\n\r\n // 添加租户ID头\r\n if (this.tenantId) {\r\n requestHeaders['tenant-id'] = this.tenantId;\r\n }\r\n\r\n addAuthHeader();\r\n\r\n // 构建请求配置\r\n const fetchOptions: RequestInit = {\r\n method,\r\n headers: requestHeaders,\r\n credentials: 'include' // 包含cookie\r\n };\r\n\r\n // 添加请求体\r\n if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {\r\n fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);\r\n }\r\n\r\n try {\r\n // 发送请求\r\n const response = await fetch(url, fetchOptions);\r\n const responseData = await this.parseResponse(response);\r\n\r\n // 检查响应状态\r\n if (!response.ok) {\r\n // 如果是401错误,尝试刷新Token并重试\r\n if (response.status === 401) {\r\n return {\r\n status: response.status,\r\n statusText: response.statusText,\r\n data: '' as T,\r\n headers: this.parseHeaders(response.headers)\r\n }\r\n }\r\n \r\n // 其他错误,直接抛出\r\n const errorMsg = this.getErrorMessage(responseData);\r\n throw new HttpError(\r\n errorMsg,\r\n response.status,\r\n response.statusText,\r\n responseData\r\n );\r\n }\r\n\r\n // 处理成功响应的业务逻辑\r\n return this.handleResponse(response, responseData);\r\n } catch (error) {\r\n if (error instanceof HttpError) {\r\n throw error;\r\n }\r\n\r\n // 网络错误或其他错误\r\n throw new HttpError(\r\n error instanceof Error ? error.message : 'Network Error',\r\n 0,\r\n 'Network Error',\r\n null\r\n );\r\n }\r\n }\r\n \r\n\r\n\r\n /**\r\n * 处理响应数据\r\n * @param response 响应对象\r\n * @param responseData 响应数据\r\n * @returns HttpResponse 处理后的响应\r\n */\r\n private handleResponse(response: Response, responseData: any): HttpResponse {\r\n // 检查是否为业务响应结构\r\n if (this.isBusinessResponse(responseData)) {\r\n // 业务响应结构:{ code, msg, data }\r\n const { code, msg, data } = responseData;\r\n \r\n // 检查业务状态码\r\n if (code !== 0 && code !== 200 && code !== '0' && code !== '200') {\r\n // 业务错误,抛出HttpError\r\n throw new HttpError(\r\n msg || `Business Error: ${code}`,\r\n response.status,\r\n response.statusText,\r\n responseData\r\n );\r\n }\r\n \r\n // 业务成功,返回data字段作为实际数据\r\n return {\r\n status: response.status,\r\n statusText: response.statusText,\r\n data: data as T,\r\n headers: this.parseHeaders(response.headers)\r\n };\r\n }\r\n \r\n // 非业务响应结构,直接返回原始数据\r\n return {\r\n status: response.status,\r\n statusText: response.statusText,\r\n data: responseData as T,\r\n headers: this.parseHeaders(response.headers)\r\n };\r\n }\r\n\r\n /**\r\n * 检查是否为业务响应结构\r\n * @param responseData 响应数据\r\n * @returns boolean 是否为业务响应结构\r\n */\r\n private isBusinessResponse(responseData: any): boolean {\r\n return typeof responseData === 'object' && \r\n responseData !== null && \r\n ('code' in responseData) && \r\n ('msg' in responseData) && \r\n ('data' in responseData);\r\n }\r\n\r\n /**\r\n * 获取错误信息\r\n * @param responseData 响应数据\r\n * @returns string 错误信息\r\n */\r\n private getErrorMessage(responseData: any): string {\r\n // 如果是业务响应结构\r\n if (this.isBusinessResponse(responseData)) {\r\n return responseData.msg || `Business Error: ${responseData.code}`;\r\n }\r\n \r\n // 其他错误结构\r\n return responseData.message || responseData.error || `HTTP Error`;\r\n }\r\n\r\n /**\r\n * GET请求\r\n * @param url 请求URL\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async get(url: string, options?: Omit): Promise> {\r\n return this.request({\r\n method: 'GET',\r\n url,\r\n ...options\r\n });\r\n }\r\n\r\n /**\r\n * POST请求\r\n * @param url 请求URL\r\n * @param body 请求体\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async post(url: string, body?: any, options?: Omit): Promise> {\r\n return this.request({\r\n method: 'POST',\r\n url,\r\n body,\r\n ...options\r\n });\r\n }\r\n\r\n /**\r\n * PUT请求\r\n * @param url 请求URL\r\n * @param body 请求体\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async put(url: string, body?: any, options?: Omit): Promise> {\r\n return this.request({\r\n method: 'PUT',\r\n url,\r\n body,\r\n ...options\r\n });\r\n }\r\n\r\n /**\r\n * DELETE请求\r\n * @param url 请求URL\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async delete(url: string, options?: Omit): Promise> {\r\n return this.request({\r\n method: 'DELETE',\r\n url,\r\n ...options\r\n });\r\n }\r\n\r\n /**\r\n * PATCH请求\r\n * @param url 请求URL\r\n * @param body 请求体\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async patch(url: string, body?: any, options?: Omit): Promise> {\r\n return this.request({\r\n method: 'PATCH',\r\n url,\r\n body,\r\n ...options\r\n });\r\n }\r\n\r\n /**\r\n * 解析响应体\r\n * @param response 响应对象\r\n * @returns Promise 解析后的响应体\r\n */\r\n private async parseResponse(response: Response): Promise {\r\n const contentType = response.headers.get('content-type') || '';\r\n \r\n if (contentType.includes('application/json')) {\r\n return response.json();\r\n } else if (contentType.includes('text/')) {\r\n return response.text();\r\n } else {\r\n return response.blob();\r\n }\r\n }\r\n\r\n /**\r\n * 解析响应头\r\n * @param headers 响应头对象\r\n * @returns Record 解析后的响应头\r\n */\r\n private parseHeaders(headers: Headers): Record {\r\n const result: Record = {};\r\n headers.forEach((value, key) => {\r\n result[key] = value;\r\n });\r\n return result;\r\n }\r\n}\r\n","/**\r\n * URL处理工具\r\n * 用于生成授权URL、解析URL参数等功能\r\n */\r\n\r\n/**\r\n * 生成随机字符串\r\n * @param length 字符串长度,默认32位\r\n * @returns 随机字符串\r\n */\r\nexport function generateRandomString(length: number = 32): string {\r\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r\n let result = '';\r\n for (let i = 0; i < length; i++) {\r\n result += chars.charAt(Math.floor(Math.random() * chars.length));\r\n }\r\n return result;\r\n}\r\n\r\n/**\r\n * 解析URL查询参数\r\n * @param url URL字符串,默认为当前URL\r\n * @returns 查询参数对象\r\n */\r\nexport function parseQueryParams(url: string = window.location.href): Record {\r\n const params: Record = {};\r\n const queryString = url.split('?')[1];\r\n if (!queryString) {\r\n return params;\r\n }\r\n\r\n const pairs = queryString.split('&');\r\n for (const pair of pairs) {\r\n const [key, value] = pair.split('=');\r\n if (key) {\r\n params[decodeURIComponent(key)] = decodeURIComponent(value || '');\r\n }\r\n }\r\n\r\n return params;\r\n}\r\n\r\n/**\r\n * 构建URL查询参数\r\n * @param params 查询参数对象\r\n * @returns 查询参数字符串\r\n */\r\nexport function buildQueryParams(params: Record): string {\r\n const pairs: string[] = [];\r\n for (const [key, value] of Object.entries(params)) {\r\n if (value !== undefined && value !== null) {\r\n pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);\r\n }\r\n }\r\n return pairs.length ? `?${pairs.join('&')}` : '';\r\n}\r\n\r\n/**\r\n * 生成OAuth2授权URL\r\n * @param authorizationEndpoint 授权端点URL\r\n * @param clientId 客户端ID\r\n * @param redirectUri 重定向URL\r\n * @param options 可选参数\r\n * @returns 授权URL\r\n */\r\nexport function generateAuthorizationUrl(\r\n authorizationEndpoint: string,\r\n clientId: string,\r\n redirectUri: string,\r\n options?: {\r\n responseType?: string;\r\n scope?: string;\r\n state?: string;\r\n [key: string]: any;\r\n }\r\n): string {\r\n const {\r\n responseType = 'code',\r\n scope,\r\n state = generateRandomString(32),\r\n ...extraParams\r\n } = options || {};\r\n\r\n const params = {\r\n client_id: clientId,\r\n redirect_uri: redirectUri,\r\n response_type: responseType,\r\n state,\r\n ...(scope ? { scope } : {}),\r\n ...extraParams\r\n };\r\n\r\n const queryString = buildQueryParams(params);\r\n return `${authorizationEndpoint}${queryString}`;\r\n}\r\n\r\n/**\r\n * 检查当前URL是否为授权回调\r\n * @param url URL字符串,默认为当前URL\r\n * @returns 是否为授权回调\r\n */\r\nexport function isCallbackUrl(url: string = window.location.href): boolean {\r\n const params = parseQueryParams(url);\r\n return !!params.code || !!params.error;\r\n}\r\n\r\n/**\r\n * 获取当前URL的路径名\r\n * @param url URL字符串,默认为当前URL\r\n * @returns 路径名\r\n */\r\nexport function getPathname(url: string = window.location.href): string {\r\n const urlObj = new URL(url);\r\n return urlObj.pathname;\r\n}\r\n\r\n/**\r\n * 获取当前URL的主机名\r\n * @param url URL字符串,默认为当前URL\r\n * @returns 主机名\r\n */\r\nexport function getHostname(url: string = window.location.href): string {\r\n const urlObj = new URL(url);\r\n return urlObj.hostname;\r\n}\r\n","/**\r\n * 认证核心逻辑\r\n * 实现OAuth2授权码模式的完整流程\r\n */\r\n\r\nimport {EventType, RouterInfo, SDKConfig, UserInfo} from '../types';\r\nimport {TokenManager} from './token';\r\nimport {HttpClient} from './http';\r\nimport {Storage} from '../utils/storage';\r\nimport {buildQueryParams, isCallbackUrl, parseQueryParams} from '../utils/url';\r\n\r\n/**\r\n * 认证核心类\r\n */\r\nexport class Auth {\r\n private config: SDKConfig | null = null;\r\n private tokenManager: TokenManager;\r\n private httpClient: HttpClient;\r\n private storage: Storage;\r\n private eventHandlers: Record = {\r\n login: [],\r\n logout: [],\r\n tokenExpired: []\r\n };\r\n private userInfoCache: UserInfo | null = null;\r\n\r\n /**\r\n * 构造函数\r\n * @param storage 存储实例\r\n */\r\n constructor(storage: Storage) {\r\n this.storage = storage;\r\n // 先创建HttpClient,初始时tokenManager为undefined\r\n this.httpClient = new HttpClient(() => this.tokenManager.getToken() || null);\r\n // 然后创建TokenManager\r\n this.tokenManager = new TokenManager(storage);\r\n }\r\n\r\n /**\r\n * 初始化SDK配置\r\n * @param config SDK配置选项\r\n */\r\n init(config: SDKConfig): void {\r\n this.config = config;\r\n // 设置租户ID到HTTP客户端\r\n this.httpClient.setTenantId(config.tenantId);\r\n }\r\n\r\n getToken():string | null{\r\n return this.tokenManager.getToken()\r\n }\r\n\r\n /**\r\n * 触发登录流程\r\n * @param redirectUri 可选的重定向URL,覆盖初始化时的配置\r\n */\r\n async login(redirectUri?: string): Promise {\r\n if (!this.config) {\r\n throw new Error('SDK not initialized');\r\n }\r\n const registrationId = this.config.registrationId || 'idp'\r\n const basepath = this.config.basepath || ''\r\n const path = `${basepath}/oauth2/authorization/${registrationId}`\r\n const tokenResponse = await this.httpClient.get(path,{needAuth:false})\r\n const redirect = tokenResponse.data.redirect_url\r\n const params = parseQueryParams(redirect)\r\n this.storage.set(params.state,window.location.href)\r\n window.location.href = redirect\r\n }\r\n\r\n /**\r\n * 退出登录\r\n */\r\n async logout(): Promise {\r\n if (!this.config) {\r\n throw new Error('SDK not initialized');\r\n }\r\n // 清除本地存储的Token和用户信息\r\n this.tokenManager.clearToken();\r\n this.userInfoCache = null;\r\n this.storage.remove('userInfo');\r\n const basepath = this.config.basepath || ''\r\n await this.httpClient.post(`${basepath}/logout`,null,{needAuth:true})\r\n // 触发退出事件\r\n this.emit('logout');\r\n window.location.href = this.config.idpLogoutUrl+'?redirect='+this.config.homePage;\r\n }\r\n\r\n /**\r\n * 处理授权回调\r\n * @returns Promise 用户信息\r\n */\r\n async handleCallback(): Promise {\r\n if (!this.config) {\r\n throw new Error('SDK not initialized');\r\n }\r\n\r\n const params = parseQueryParams();\r\n \r\n // 检查是否有错误\r\n if (params.error) {\r\n throw new Error(`Authorization error: ${params.error} - ${params.error_description || ''}`);\r\n }\r\n\r\n // 检查是否有授权码\r\n if (!params.code) {\r\n throw new Error('Authorization code not found');\r\n }\r\n\r\n const registrationId = this.config.registrationId || 'idp'\r\n const basepath = this.config.basepath || ''\r\n const callback = `${basepath}/login/oauth2/code/${registrationId}${buildQueryParams(params)}`\r\n const tokenResponse = await this.httpClient.get(callback,{\r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded'\r\n },\r\n needAuth: false\r\n })\r\n // 触发登录事件\r\n this.emit('login');\r\n this.storage.set('userInfo', tokenResponse.data.data);\r\n this.tokenManager.saveToken(tokenResponse.headers['authorization']||tokenResponse.headers['Authorization'])\r\n let url = this.config.homePage\r\n if(params.state){\r\n url = this.storage.get(params.state) || url;\r\n }\r\n window.location.href = url;\r\n\r\n }\r\n\r\n async getRoutes(): Promise {\r\n if (!this.config) {\r\n throw new Error('SDK not initialized');\r\n }\r\n const basepath = this.config.basepath || ''\r\n const tokenResponse = await this.httpClient.get(`${basepath}/idp/routes`,{needAuth:true})\r\n if(tokenResponse.status===401){\r\n await this.logout()\r\n }\r\n return tokenResponse.data.data\r\n\r\n }\r\n /**\r\n * 获取用户信息\r\n * @returns UserInfo 用户信息\r\n */\r\n getUserInfo(): UserInfo {\r\n return this.storage.get(\"userInfo\");\r\n }\r\n\r\n\r\n\r\n\r\n /**\r\n * 检查用户是否有指定角色\r\n * @param role 角色编码或角色编码列表\r\n * @returns Promise 是否有指定角色\r\n */\r\n async hasRole(role: string | string[]): Promise {\r\n if (!this.isAuthenticated()) {\r\n return false;\r\n }\r\n\r\n const userInfo:UserInfo = this.storage.get(\"userInfo\");\r\n const roleCodes = userInfo.roles||[];\r\n\r\n if (Array.isArray(role)) {\r\n // 检查是否有任一角色\r\n return role.some(r => roleCodes.includes(r));\r\n }\r\n\r\n // 检查是否有单个角色\r\n return roleCodes.includes(role);\r\n }\r\n\r\n /**\r\n * 检查用户是否有所有指定角色\r\n * @param roles 角色编码列表\r\n * @returns Promise 是否有所有指定角色\r\n */\r\n async hasAllRoles(roles: string[]): Promise {\r\n if (!this.isAuthenticated()) {\r\n return false;\r\n }\r\n\r\n const userInfo:UserInfo = this.storage.get(\"userInfo\");\r\n const roleCodes = userInfo.roles||[];\r\n // 检查是否有所有角色\r\n return roles.every(r => roleCodes.includes(r));\r\n }\r\n\r\n /**\r\n * 检查用户是否有指定权限\r\n * @param permission 权限标识或权限标识列表\r\n * @returns Promise 是否有指定权限\r\n */\r\n async hasPermission(permission: string | string[]): Promise {\r\n if (!this.isAuthenticated()) {\r\n return false;\r\n }\r\n\r\n const userInfo:UserInfo = this.storage.get(\"userInfo\");\r\n const permissions = userInfo.permissions||[];\r\n\r\n if (Array.isArray(permission)) {\r\n // 检查是否有任一权限\r\n return permission.some(p => permissions.includes(p));\r\n }\r\n\r\n // 检查是否有单个权限\r\n return permissions.includes(permission);\r\n }\r\n\r\n /**\r\n * 检查用户是否有所有指定权限\r\n * @param permissions 权限标识列表\r\n * @returns Promise 是否有所有指定权限\r\n */\r\n async hasAllPermissions(permissions: string[]): Promise {\r\n if (!this.isAuthenticated()) {\r\n return false;\r\n }\r\n\r\n const userInfo:UserInfo = this.storage.get(\"userInfo\");\r\n const userPermissions = userInfo.permissions||[];\r\n\r\n // 检查是否有所有权限\r\n return permissions.every(p => userPermissions.includes(p));\r\n }\r\n\r\n /**\r\n * 检查用户是否已认证\r\n * @returns boolean 是否已认证\r\n */\r\n isAuthenticated(): boolean {\r\n // 检查Token是否存在且未过期\r\n return !!this.tokenManager.getToken();\r\n }\r\n\r\n /**\r\n * 事件监听\r\n * @param event 事件类型\r\n * @param callback 回调函数\r\n */\r\n on(event: EventType, callback: Function): void {\r\n this.eventHandlers[event].push(callback);\r\n }\r\n\r\n /**\r\n * 移除事件监听\r\n * @param event 事件类型\r\n * @param callback 回调函数\r\n */\r\n off(event: EventType, callback: Function): void {\r\n this.eventHandlers[event] = this.eventHandlers[event].filter(handler => handler !== callback);\r\n }\r\n\r\n /**\r\n * 触发事件\r\n * @param event 事件类型\r\n * @param data 事件数据\r\n */\r\n private emit(event: EventType, data?: any): void {\r\n this.eventHandlers[event].forEach(handler => {\r\n try {\r\n handler(data);\r\n } catch (error) {\r\n console.error(`Error in ${event} event handler:`, error);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * 检查当前URL是否为授权回调\r\n * @returns boolean 是否为授权回调\r\n */\r\n isCallback(): boolean {\r\n return isCallbackUrl();\r\n }\r\n}\r\n","/**\r\n * 存储工具类\r\n * 支持localStorage、sessionStorage和cookie三种存储方式\r\n */\r\n\r\ntype StorageType = 'localStorage' | 'sessionStorage' | 'cookie';\r\n\r\n/**\r\n * 存储工具类\r\n */\r\nexport class Storage {\r\n private storageType: StorageType;\r\n private prefix: string;\r\n\r\n /**\r\n * 构造函数\r\n * @param storageType 存储类型\r\n * @param prefix 存储前缀,默认'unified_login_'\r\n */\r\n constructor(storageType: StorageType = 'localStorage', prefix: string = 'unified_login_') {\r\n this.storageType = storageType;\r\n this.prefix = prefix;\r\n }\r\n\r\n /**\r\n * 设置存储项\r\n * @param key 存储键\r\n * @param value 存储值\r\n * @param options 可选参数,cookie存储时使用\r\n */\r\n set(key: string, value: any, options?: { expires?: number; path?: string; domain?: string; secure?: boolean }): void {\r\n const fullKey = this.prefix + key;\r\n const stringValue = typeof value === 'string' ? value : JSON.stringify(value);\r\n\r\n switch (this.storageType) {\r\n case 'localStorage':\r\n this.setLocalStorage(fullKey, stringValue);\r\n break;\r\n case 'sessionStorage':\r\n this.setSessionStorage(fullKey, stringValue);\r\n break;\r\n case 'cookie':\r\n this.setCookie(fullKey, stringValue, options);\r\n break;\r\n }\r\n }\r\n\r\n /**\r\n * 获取存储项\r\n * @param key 存储键\r\n * @returns 存储值\r\n */\r\n get(key: string): any {\r\n const fullKey = this.prefix + key;\r\n let value: any;\r\n\r\n switch (this.storageType) {\r\n case 'localStorage':\r\n value = this.getLocalStorage(fullKey);\r\n break;\r\n case 'sessionStorage':\r\n value = this.getSessionStorage(fullKey);\r\n break;\r\n case 'cookie':\r\n value = this.getCookie(fullKey);\r\n break;\r\n default:\r\n value = null;\r\n }\r\n\r\n if (value === null) {\r\n return null;\r\n }\r\n\r\n // 尝试解析JSON\r\n try {\r\n return JSON.parse(value);\r\n } catch (e) {\r\n // 如果不是JSON,直接返回字符串\r\n return value;\r\n }\r\n }\r\n\r\n /**\r\n * 移除存储项\r\n * @param key 存储键\r\n */\r\n remove(key: string): void {\r\n const fullKey = this.prefix + key;\r\n\r\n switch (this.storageType) {\r\n case 'localStorage':\r\n this.removeLocalStorage(fullKey);\r\n break;\r\n case 'sessionStorage':\r\n this.removeSessionStorage(fullKey);\r\n break;\r\n case 'cookie':\r\n this.removeCookie(fullKey);\r\n break;\r\n }\r\n }\r\n\r\n /**\r\n * 清空所有存储项\r\n */\r\n clear(): void {\r\n switch (this.storageType) {\r\n case 'localStorage':\r\n this.clearLocalStorage();\r\n break;\r\n case 'sessionStorage':\r\n this.clearSessionStorage();\r\n break;\r\n case 'cookie':\r\n this.clearCookie();\r\n break;\r\n }\r\n }\r\n\r\n /**\r\n * 检查存储类型是否可用\r\n * @returns boolean 是否可用\r\n */\r\n isAvailable(): boolean {\r\n try {\r\n switch (this.storageType) {\r\n case 'localStorage':\r\n return this.isLocalStorageAvailable();\r\n case 'sessionStorage':\r\n return this.isSessionStorageAvailable();\r\n case 'cookie':\r\n return typeof document !== 'undefined';\r\n default:\r\n return false;\r\n }\r\n } catch (e) {\r\n return false;\r\n }\r\n }\r\n\r\n // ------------------------ localStorage 操作 ------------------------\r\n\r\n /**\r\n * 设置localStorage\r\n */\r\n private setLocalStorage(key: string, value: string): void {\r\n if (this.isLocalStorageAvailable()) {\r\n localStorage.setItem(key, value);\r\n }\r\n }\r\n\r\n /**\r\n * 获取localStorage\r\n */\r\n private getLocalStorage(key: string): string | null {\r\n if (this.isLocalStorageAvailable()) {\r\n return localStorage.getItem(key);\r\n }\r\n return null;\r\n }\r\n\r\n /**\r\n * 移除localStorage\r\n */\r\n private removeLocalStorage(key: string): void {\r\n if (this.isLocalStorageAvailable()) {\r\n localStorage.removeItem(key);\r\n }\r\n }\r\n\r\n /**\r\n * 清空localStorage中所有带前缀的项\r\n */\r\n private clearLocalStorage(): void {\r\n if (this.isLocalStorageAvailable()) {\r\n for (let i = 0; i < localStorage.length; i++) {\r\n const key = localStorage.key(i);\r\n if (key && key.startsWith(this.prefix)) {\r\n localStorage.removeItem(key);\r\n i--; // 索引调整\r\n }\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * 检查localStorage是否可用\r\n */\r\n private isLocalStorageAvailable(): boolean {\r\n if (typeof localStorage === 'undefined') {\r\n return false;\r\n }\r\n try {\r\n const testKey = '__storage_test__';\r\n localStorage.setItem(testKey, testKey);\r\n localStorage.removeItem(testKey);\r\n return true;\r\n } catch (e) {\r\n return false;\r\n }\r\n }\r\n\r\n // ------------------------ sessionStorage 操作 ------------------------\r\n\r\n /**\r\n * 设置sessionStorage\r\n */\r\n private setSessionStorage(key: string, value: string): void {\r\n if (this.isSessionStorageAvailable()) {\r\n sessionStorage.setItem(key, value);\r\n }\r\n }\r\n\r\n /**\r\n * 获取sessionStorage\r\n */\r\n private getSessionStorage(key: string): string | null {\r\n if (this.isSessionStorageAvailable()) {\r\n return sessionStorage.getItem(key);\r\n }\r\n return null;\r\n }\r\n\r\n /**\r\n * 移除sessionStorage\r\n */\r\n private removeSessionStorage(key: string): void {\r\n if (this.isSessionStorageAvailable()) {\r\n sessionStorage.removeItem(key);\r\n }\r\n }\r\n\r\n /**\r\n * 清空sessionStorage中所有带前缀的项\r\n */\r\n private clearSessionStorage(): void {\r\n if (this.isSessionStorageAvailable()) {\r\n for (let i = 0; i < sessionStorage.length; i++) {\r\n const key = sessionStorage.key(i);\r\n if (key && key.startsWith(this.prefix)) {\r\n sessionStorage.removeItem(key);\r\n i--; // 索引调整\r\n }\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * 检查sessionStorage是否可用\r\n */\r\n private isSessionStorageAvailable(): boolean {\r\n if (typeof sessionStorage === 'undefined') {\r\n return false;\r\n }\r\n try {\r\n const testKey = '__storage_test__';\r\n sessionStorage.setItem(testKey, testKey);\r\n sessionStorage.removeItem(testKey);\r\n return true;\r\n } catch (e) {\r\n return false;\r\n }\r\n }\r\n\r\n // ------------------------ cookie 操作 ------------------------\r\n\r\n /**\r\n * 设置cookie\r\n */\r\n private setCookie(\r\n key: string,\r\n value: string,\r\n options?: { expires?: number; path?: string; domain?: string; secure?: boolean }\r\n ): void {\r\n if (typeof document === 'undefined') {\r\n return;\r\n }\r\n\r\n let cookieString = `${key}=${encodeURIComponent(value)}`;\r\n\r\n if (options) {\r\n // 设置过期时间(秒)\r\n if (options.expires) {\r\n const date = new Date();\r\n date.setTime(date.getTime() + options.expires * 1000);\r\n cookieString += `; expires=${date.toUTCString()}`;\r\n }\r\n\r\n // 设置路径\r\n if (options.path) {\r\n cookieString += `; path=${options.path}`;\r\n }\r\n\r\n // 设置域名\r\n if (options.domain) {\r\n cookieString += `; domain=${options.domain}`;\r\n }\r\n\r\n // 设置secure\r\n if (options.secure) {\r\n cookieString += '; secure';\r\n }\r\n }\r\n\r\n document.cookie = cookieString;\r\n }\r\n\r\n /**\r\n * 获取cookie\r\n */\r\n private getCookie(key: string): string | null {\r\n if (typeof document === 'undefined') {\r\n return null;\r\n }\r\n\r\n const name = `${key}=`;\r\n const decodedCookie = decodeURIComponent(document.cookie);\r\n const ca = decodedCookie.split(';');\r\n\r\n for (let i = 0; i < ca.length; i++) {\r\n let c = ca[i];\r\n while (c.charAt(0) === ' ') {\r\n c = c.substring(1);\r\n }\r\n if (c.indexOf(name) === 0) {\r\n return c.substring(name.length, c.length);\r\n }\r\n }\r\n\r\n return null;\r\n }\r\n\r\n /**\r\n * 移除cookie\r\n */\r\n private removeCookie(key: string): void {\r\n this.setCookie(key, '', { expires: -1 });\r\n }\r\n\r\n /**\r\n * 清空所有带前缀的cookie\r\n */\r\n private clearCookie(): void {\r\n if (typeof document === 'undefined') {\r\n return;\r\n }\r\n\r\n const cookies = document.cookie.split(';');\r\n for (const cookie of cookies) {\r\n const eqPos = cookie.indexOf('=');\r\n const key = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();\r\n if (key.startsWith(this.prefix)) {\r\n this.removeCookie(key);\r\n }\r\n }\r\n }\r\n}\r\n","/**\r\n * 路由守卫模块\r\n * 提供基于权限的路由拦截和未登录自动跳转登录页功能\r\n */\r\n\r\nimport { Auth } from '../core/auth';\r\n\r\n/**\r\n * 路由守卫选项\r\n */\r\nexport interface RouterGuardOptions {\r\n /**\r\n * 是否需要登录\r\n */\r\n requiresAuth?: boolean;\r\n /**\r\n * 需要的权限列表\r\n */\r\n requiredPermissions?: string[];\r\n /**\r\n * 登录后重定向的URL\r\n */\r\n redirectUri?: string;\r\n /**\r\n * 权限不足时重定向的URL\r\n */\r\n unauthorizedRedirectUri?: string;\r\n}\r\n\r\n/**\r\n * 路由守卫类\r\n */\r\nexport class RouterGuard {\r\n private auth: Auth;\r\n\r\n /**\r\n * 构造函数\r\n * @param auth 认证实例\r\n */\r\n constructor(auth: Auth) {\r\n this.auth = auth;\r\n }\r\n\r\n /**\r\n * 检查路由权限\r\n * @param options 路由守卫选项\r\n * @returns Promise 是否通过权限检查\r\n */\r\n async check(options: RouterGuardOptions): Promise {\r\n const { requiresAuth = true, requiredPermissions = [] } = options;\r\n\r\n // 检查是否需要登录\r\n if (requiresAuth) {\r\n // 检查是否已认证\r\n if (!this.auth.isAuthenticated()) {\r\n // 未认证,跳转到登录页\r\n this.auth.login(options.redirectUri);\r\n return false;\r\n }\r\n\r\n // 检查是否需要权限\r\n if (requiredPermissions.length > 0) {\r\n // 获取用户权限\r\n const userPermissions = [''];\r\n \r\n // 检查是否拥有所有需要的权限\r\n const hasPermission = requiredPermissions.every(permission => \r\n userPermissions.includes(permission)\r\n );\r\n\r\n if (!hasPermission) {\r\n // 权限不足,跳转到权限不足页\r\n if (options.unauthorizedRedirectUri) {\r\n window.location.href = options.unauthorizedRedirectUri;\r\n }\r\n return false;\r\n }\r\n }\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * 创建Vue路由守卫\r\n * @returns 路由守卫函数\r\n */\r\n createVueGuard() {\r\n return async (to: any, from: any, next: any) => {\r\n // 从路由元信息中获取守卫选项\r\n const options: RouterGuardOptions = to.meta?.auth || {};\r\n \r\n try {\r\n const allowed = await this.check(options);\r\n if (allowed) {\r\n next();\r\n }\r\n } catch (error) {\r\n console.error('Route guard error:', error);\r\n next(false);\r\n }\r\n };\r\n }\r\n\r\n /**\r\n * 检查当前用户是否有权限访问资源\r\n * @param permissions 需要的权限列表\r\n * @returns Promise 是否拥有权限\r\n */\r\n async hasPermission(permissions: string | string[]): Promise {\r\n if (!permissions) {\r\n return true;\r\n }\r\n\r\n const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions];\r\n \r\n // 检查是否已认证\r\n if (!this.auth.isAuthenticated()) {\r\n return false;\r\n }\r\n\r\n // 获取用户权限\r\n const userPermissions = ['']\r\n \r\n // 检查是否拥有所有需要的权限\r\n return requiredPermissions.every(permission => \r\n userPermissions.includes(permission)\r\n );\r\n }\r\n}\r\n","/**\r\n * Vue插件模块\r\n * 提供Vue应用中使用统一登录SDK的能力\r\n */\r\n\r\nimport { Auth } from '../core/auth';\r\nimport { SDKConfig } from '../types';\r\nimport { Storage } from '../utils/storage';\r\nimport { RouterGuard } from '../guards/router';\r\n\r\n/**\r\n * Vue插件选项\r\n */\r\nexport interface VuePluginOptions {\r\n /**\r\n * SDK配置\r\n */\r\n config: SDKConfig;\r\n /**\r\n * 插件名称,默认'unifiedLogin'\r\n */\r\n pluginName?: string;\r\n}\r\n\r\n/**\r\n * Vue插件类\r\n */\r\nexport class VuePlugin {\r\n private auth: Auth;\r\n private routerGuard: RouterGuard;\r\n\r\n /**\r\n * 构造函数\r\n * @param storage 存储实例\r\n */\r\n constructor(storage: Storage) {\r\n this.auth = new Auth(storage);\r\n this.routerGuard = new RouterGuard(this.auth);\r\n }\r\n\r\n /**\r\n * 安装Vue插件\r\n * @param app Vue构造函数或Vue 3应用实例\r\n * @param options 插件选项\r\n */\r\n install(app: any, options: VuePluginOptions): void {\r\n const { config, pluginName = 'unifiedLogin' } = options;\r\n\r\n // 初始化SDK\r\n this.auth.init(config);\r\n\r\n // 判断是Vue 2还是Vue 3\r\n const isVue3 = typeof app.config !== 'undefined';\r\n\r\n if (isVue3) {\r\n // Vue 3\r\n // 在全局属性上挂载SDK实例\r\n app.config.globalProperties[`${pluginName}`] = this.auth;\r\n app.config.globalProperties.$auth = this.auth; // 兼容简写\r\n\r\n // 提供Vue组件内的注入\r\n app.provide(pluginName, this.auth);\r\n app.provide('auth', this.auth); // 兼容简写\r\n\r\n // 处理路由守卫\r\n app.mixin({\r\n beforeCreate() {\r\n // 如果是根组件,添加路由守卫\r\n if (this.$options.router) {\r\n const router = this.$options.router;\r\n // 添加全局前置守卫\r\n router.beforeEach(this.routerGuard.createVueGuard());\r\n }\r\n }\r\n });\r\n } else {\r\n // Vue 2\r\n // 在Vue实例上挂载SDK实例\r\n app.prototype[`${pluginName}`] = this.auth;\r\n app.prototype.$auth = this.auth; // 兼容简写\r\n\r\n // 全局混入\r\n app.mixin({\r\n beforeCreate() {\r\n // 如果是根组件,添加路由守卫\r\n if (this.$options.router) {\r\n const router = this.$options.router;\r\n // 添加全局前置守卫\r\n router.beforeEach(this.routerGuard.createVueGuard());\r\n }\r\n }\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * 获取认证实例\r\n * @returns Auth 认证实例\r\n */\r\n getAuth(): Auth {\r\n return this.auth;\r\n }\r\n\r\n /**\r\n * 获取路由守卫实例\r\n * @returns RouterGuard 路由守卫实例\r\n */\r\n getRouterGuard(): RouterGuard {\r\n return this.routerGuard;\r\n }\r\n}\r\n\r\n/**\r\n * 创建Vue插件实例\r\n * @param storageType 存储类型\r\n * @returns VuePlugin Vue插件实例\r\n */\r\nexport function createVuePlugin(storageType?: 'localStorage' | 'sessionStorage' | 'cookie'): VuePlugin {\r\n const storage = new Storage(storageType);\r\n return new VuePlugin(storage);\r\n}\r\n","/**\r\n * 统一登录SDK入口文件\r\n * 支持OAuth2授权码模式,提供完整的Token管理和用户信息管理功能\r\n */\r\n\r\n// 导出核心类和功能\r\nexport { Auth } from './core/auth';\r\nexport { TokenManager } from './core/token';\r\nexport { HttpClient, HttpError } from './core/http';\r\nexport { Storage } from './utils/storage';\r\nexport { RouterGuard, RouterGuardOptions } from './guards/router';\r\n\r\n// 导出工具函数\r\nexport { \r\n generateRandomString, \r\n parseQueryParams, \r\n buildQueryParams, \r\n generateAuthorizationUrl, \r\n isCallbackUrl \r\n} from './utils/url';\r\n\r\n// 导出类型定义\r\nexport * from './types';\r\n\r\n// 导出Vue插件\r\nexport { VuePlugin, createVuePlugin } from './plugins/vue';\r\n\r\n// 创建默认SDK实例\r\nimport { SDKConfig, UnifiedLoginSDK } from './types';\r\nimport { Auth as AuthCore } from './core/auth';\r\nimport { Storage as StorageCore } from './utils/storage';\r\n\r\n/**\r\n * 默认SDK实例\r\n */\r\nconst defaultStorage = new StorageCore();\r\nconst defaultAuth = new AuthCore(defaultStorage);\r\n\r\n/**\r\n * 默认导出的SDK实例\r\n */\r\nexport const unifiedLoginSDK: UnifiedLoginSDK = {\r\n init: (config: SDKConfig) => {\r\n defaultAuth.init(config);\r\n },\r\n getToken: () => {\r\n return defaultAuth.getToken()\r\n },\r\n login: (redirectUri?: string) => {\r\n return defaultAuth.login(redirectUri);\r\n },\r\n logout: () => {\r\n return defaultAuth.logout();\r\n },\r\n handleCallback: () => {\r\n return defaultAuth.handleCallback();\r\n },\r\n getRoutes: () => {\r\n return defaultAuth.getRoutes();\r\n },\r\n getUserInfo: () => {\r\n return defaultAuth.getUserInfo();\r\n },\r\n isAuthenticated: () => {\r\n return defaultAuth.isAuthenticated();\r\n },\r\n hasRole: (role: string | string[]) => {\r\n return defaultAuth.hasRole(role);\r\n },\r\n hasAllRoles: (roles: string[]) => {\r\n return defaultAuth.hasAllRoles(roles);\r\n },\r\n hasPermission: (permission: string | string[]) => {\r\n return defaultAuth.hasPermission(permission);\r\n },\r\n hasAllPermissions: (permissions: string[]) => {\r\n return defaultAuth.hasAllPermissions(permissions);\r\n },\r\n on: (event, callback) => {\r\n return defaultAuth.on(event, callback);\r\n },\r\n off: (event, callback) => {\r\n return defaultAuth.off(event, callback);\r\n },\r\n isCallback: () => {\r\n return defaultAuth.isCallback();\r\n }\r\n};\r\n\r\n// 默认导出\r\nexport default unifiedLoginSDK;\r\n\r\n// 版本信息\r\nexport const version = '1.0.0';\r\n"],"names":["StorageCore","AuthCore"],"mappings":"AAAA;;;AAGG;AAIH;;AAEG;MACU,YAAY,CAAA;AAGvB;;;;AAIG;AACH,IAAA,WAAA,CAAY,OAAgB,EAAA;AAC1B,QAAA,IAAI,CAAC,OAAO,GAAG,OAAO;IACxB;AAEA;;;AAGG;AACH,IAAA,SAAS,CAAC,SAAiB,EAAA;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,CAAC;IACtC;AAEA;;;AAGG;IACH,QAAQ,GAAA;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;IAClC;AAEA;;AAEG;IACH,UAAU,GAAA;AACR,QAAA,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;IAC9B;AACD;;AC5CD;;;AAGG;AAqCH;;AAEG;AACG,MAAO,SAAU,SAAQ,KAAK,CAAA;AAQlC;;;;;;AAMG;AACH,IAAA,WAAA,CAAY,OAAe,EAAE,MAAc,EAAE,UAAkB,EAAE,IAAS,EAAA;QACxE,KAAK,CAAC,OAAO,CAAC;AACd,QAAA,IAAI,CAAC,IAAI,GAAG,WAAW;AACvB,QAAA,IAAI,CAAC,MAAM,GAAG,MAAM;AACpB,QAAA,IAAI,CAAC,UAAU,GAAG,UAAU;AAC5B,QAAA,IAAI,CAAC,IAAI,GAAG,IAAI;IAClB;AACD;AAED;;AAEG;MACU,UAAU,CAAA;AAIrB;;;;AAIG;AACH,IAAA,WAAA,CAAY,WAAiC,EAAA;AAC3C,QAAA,IAAI,CAAC,WAAW,GAAG,WAAW;IAEhC;AAEA;;;AAGG;AACH,IAAA,cAAc,CAAC,WAAgC,EAAA;AAC7C,QAAA,IAAI,CAAC,WAAW,GAAG,WAAW;IAChC;AAEA;;;AAGG;AACH,IAAA,WAAW,CAAC,QAAiB,EAAA;AAC3B,QAAA,IAAI,CAAC,QAAQ,GAAG,QAAQ;IAC1B;AAEA;;;;AAIG;IACH,MAAM,OAAO,CAAU,OAA2B,EAAA;AAChD,QAAA,MAAM,EACJ,MAAM,EACN,GAAG,EACH,OAAO,GAAG,EAAE,EACZ,IAAI,EACJ,QAAQ,GAAG,IAAI,EAChB,GAAG,OAAO;;AAGX,QAAA,MAAM,cAAc,GAA2B;AAC7C,YAAA,cAAc,EAAE,kBAAkB;AAClC,YAAA,GAAG;SACJ;;QAGD,MAAM,aAAa,GAAG,MAAK;AACzB,YAAA,IAAI,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE;AAChC,gBAAA,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE;gBAChC,IAAI,KAAK,EAAE;AACT,oBAAA,cAAc,CAAC,aAAa,GAAG,CAAA,EAAG,KAAK,EAAE;gBAC3C;YACF;AACF,QAAA,CAAC;;AAGD,QAAA,IAAI,IAAI,CAAC,QAAQ,EAAE;AACjB,YAAA,cAAc,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,QAAQ;QAC7C;AAEA,QAAA,aAAa,EAAE;;AAGf,QAAA,MAAM,YAAY,GAAgB;YAChC,MAAM;AACN,YAAA,OAAO,EAAE,cAAc;YACvB,WAAW,EAAE,SAAS;SACvB;;AAGD,QAAA,IAAI,IAAI,KAAK,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,OAAO,CAAC,EAAE;YACzE,YAAY,CAAC,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAC5E;AAEA,QAAA,IAAI;;YAEF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC;YAC/C,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC;;AAGvD,YAAA,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE;;AAEhB,gBAAA,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE;oBAC3B,OAAO;wBACL,MAAM,EAAE,QAAQ,CAAC,MAAM;wBACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;AAC/B,wBAAA,IAAI,EAAE,EAAO;wBACb,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO;qBAC5C;gBACH;;gBAGA,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC;AACnD,gBAAA,MAAM,IAAI,SAAS,CACjB,QAAQ,EACR,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,UAAU,EACnB,YAAY,CACb;YACH;;YAGA,OAAO,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,YAAY,CAAC;QACpD;QAAE,OAAO,KAAK,EAAE;AACd,YAAA,IAAI,KAAK,YAAY,SAAS,EAAE;AAC9B,gBAAA,MAAM,KAAK;YACb;;YAGA,MAAM,IAAI,SAAS,CACjB,KAAK,YAAY,KAAK,GAAG,KAAK,CAAC,OAAO,GAAG,eAAe,EACxD,CAAC,EACD,eAAe,EACf,IAAI,CACL;QACH;IACF;AAIA;;;;;AAKG;IACK,cAAc,CAAU,QAAkB,EAAE,YAAiB,EAAA;;AAEnE,QAAA,IAAI,IAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE;;YAEzC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,YAAY;;AAGxC,YAAA,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,KAAK,EAAE;;AAEhE,gBAAA,MAAM,IAAI,SAAS,CACjB,GAAG,IAAI,CAAA,gBAAA,EAAmB,IAAI,CAAA,CAAE,EAChC,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,UAAU,EACnB,YAAY,CACb;YACH;;YAGA,OAAO;gBACL,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;AAC/B,gBAAA,IAAI,EAAE,IAAS;gBACf,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO;aAC5C;QACH;;QAGA,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;AAC/B,YAAA,IAAI,EAAE,YAAiB;YACvB,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO;SAC5C;IACH;AAEA;;;;AAIG;AACK,IAAA,kBAAkB,CAAC,YAAiB,EAAA;QAC1C,OAAO,OAAO,YAAY,KAAK,QAAQ;AAChC,YAAA,YAAY,KAAK,IAAI;aACpB,MAAM,IAAI,YAAY,CAAC;aACvB,KAAK,IAAI,YAAY,CAAC;AACvB,aAAC,MAAM,IAAI,YAAY,CAAC;IACjC;AAEA;;;;AAIG;AACK,IAAA,eAAe,CAAC,YAAiB,EAAA;;AAEvC,QAAA,IAAI,IAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE;YACzC,OAAO,YAAY,CAAC,GAAG,IAAI,mBAAmB,YAAY,CAAC,IAAI,CAAA,CAAE;QACnE;;QAGA,OAAO,YAAY,CAAC,OAAO,IAAI,YAAY,CAAC,KAAK,IAAI,CAAA,UAAA,CAAY;IACnE;AAEA;;;;;AAKG;AACH,IAAA,MAAM,GAAG,CAAU,GAAW,EAAE,OAAoD,EAAA;QAClF,OAAO,IAAI,CAAC,OAAO,CAAI;AACrB,YAAA,MAAM,EAAE,KAAK;YACb,GAAG;AACH,YAAA,GAAG;AACJ,SAAA,CAAC;IACJ;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,IAAI,CAAU,GAAW,EAAE,IAAU,EAAE,OAA6D,EAAA;QACxG,OAAO,IAAI,CAAC,OAAO,CAAI;AACrB,YAAA,MAAM,EAAE,MAAM;YACd,GAAG;YACH,IAAI;AACJ,YAAA,GAAG;AACJ,SAAA,CAAC;IACJ;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,GAAG,CAAU,GAAW,EAAE,IAAU,EAAE,OAA6D,EAAA;QACvG,OAAO,IAAI,CAAC,OAAO,CAAI;AACrB,YAAA,MAAM,EAAE,KAAK;YACb,GAAG;YACH,IAAI;AACJ,YAAA,GAAG;AACJ,SAAA,CAAC;IACJ;AAEA;;;;;AAKG;AACH,IAAA,MAAM,MAAM,CAAU,GAAW,EAAE,OAAoD,EAAA;QACrF,OAAO,IAAI,CAAC,OAAO,CAAI;AACrB,YAAA,MAAM,EAAE,QAAQ;YAChB,GAAG;AACH,YAAA,GAAG;AACJ,SAAA,CAAC;IACJ;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,KAAK,CAAU,GAAW,EAAE,IAAU,EAAE,OAA6D,EAAA;QACzG,OAAO,IAAI,CAAC,OAAO,CAAI;AACrB,YAAA,MAAM,EAAE,OAAO;YACf,GAAG;YACH,IAAI;AACJ,YAAA,GAAG;AACJ,SAAA,CAAC;IACJ;AAEA;;;;AAIG;IACK,MAAM,aAAa,CAAC,QAAkB,EAAA;AAC5C,QAAA,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE;AAE9D,QAAA,IAAI,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE;AAC5C,YAAA,OAAO,QAAQ,CAAC,IAAI,EAAE;QACxB;AAAO,aAAA,IAAI,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE;AACxC,YAAA,OAAO,QAAQ,CAAC,IAAI,EAAE;QACxB;aAAO;AACL,YAAA,OAAO,QAAQ,CAAC,IAAI,EAAE;QACxB;IACF;AAEA;;;;AAIG;AACK,IAAA,YAAY,CAAC,OAAgB,EAAA;QACnC,MAAM,MAAM,GAA2B,EAAE;QACzC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,KAAI;AAC7B,YAAA,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK;AACrB,QAAA,CAAC,CAAC;AACF,QAAA,OAAO,MAAM;IACf;AACD;;ACjXD;;;AAGG;AAEH;;;;AAIG;AACG,SAAU,oBAAoB,CAAC,MAAA,GAAiB,EAAE,EAAA;IACtD,MAAM,KAAK,GAAG,gEAAgE;IAC9E,IAAI,MAAM,GAAG,EAAE;AACf,IAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE;AAC/B,QAAA,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAClE;AACA,IAAA,OAAO,MAAM;AACf;AAEA;;;;AAIG;AACG,SAAU,gBAAgB,CAAC,GAAA,GAAc,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAA;IACjE,MAAM,MAAM,GAA2B,EAAE;IACzC,MAAM,WAAW,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACrC,IAAI,CAAC,WAAW,EAAE;AAChB,QAAA,OAAO,MAAM;IACf;IAEA,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC;AACpC,IAAA,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE;AACxB,QAAA,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;QACpC,IAAI,GAAG,EAAE;AACP,YAAA,MAAM,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,GAAG,kBAAkB,CAAC,KAAK,IAAI,EAAE,CAAC;QACnE;IACF;AAEA,IAAA,OAAO,MAAM;AACf;AAEA;;;;AAIG;AACG,SAAU,gBAAgB,CAAC,MAA2B,EAAA;IAC1D,MAAM,KAAK,GAAa,EAAE;AAC1B,IAAA,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;QACjD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE;AACzC,YAAA,KAAK,CAAC,IAAI,CAAC,CAAA,EAAG,kBAAkB,CAAC,GAAG,CAAC,CAAA,CAAA,EAAI,kBAAkB,CAAC,KAAK,CAAC,CAAA,CAAE,CAAC;QACvE;IACF;AACA,IAAA,OAAO,KAAK,CAAC,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA,CAAE,GAAG,EAAE;AAClD;AAEA;;;;;;;AAOG;AACG,SAAU,wBAAwB,CACtC,qBAA6B,EAC7B,QAAgB,EAChB,WAAmB,EACnB,OAKC,EAAA;IAED,MAAM,EACJ,YAAY,GAAG,MAAM,EACrB,KAAK,EACL,KAAK,GAAG,oBAAoB,CAAC,EAAE,CAAC,EAChC,GAAG,WAAW,EACf,GAAG,OAAO,IAAI,EAAE;AAEjB,IAAA,MAAM,MAAM,GAAG;AACb,QAAA,SAAS,EAAE,QAAQ;AACnB,QAAA,YAAY,EAAE,WAAW;AACzB,QAAA,aAAa,EAAE,YAAY;QAC3B,KAAK;AACL,QAAA,IAAI,KAAK,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;AAC3B,QAAA,GAAG;KACJ;AAED,IAAA,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC;AAC5C,IAAA,OAAO,CAAA,EAAG,qBAAqB,CAAA,EAAG,WAAW,EAAE;AACjD;AAEA;;;;AAIG;AACG,SAAU,aAAa,CAAC,GAAA,GAAc,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAA;AAC9D,IAAA,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC;IACpC,OAAO,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK;AACxC;;ACxGA;;;AAGG;AAQH;;AAEG;MACU,IAAI,CAAA;AAYf;;;AAGG;AACH,IAAA,WAAA,CAAY,OAAgB,EAAA;QAfpB,IAAA,CAAA,MAAM,GAAqB,IAAI;AAI/B,QAAA,IAAA,CAAA,aAAa,GAAkC;AACrD,YAAA,KAAK,EAAE,EAAE;AACT,YAAA,MAAM,EAAE,EAAE;AACV,YAAA,YAAY,EAAE;SACf;QACO,IAAA,CAAA,aAAa,GAAoB,IAAI;AAO3C,QAAA,IAAI,CAAC,OAAO,GAAG,OAAO;;AAEtB,QAAA,IAAI,CAAC,UAAU,GAAG,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,IAAI,CAAC;;QAE5E,IAAI,CAAC,YAAY,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC;IAC/C;AAEA;;;AAGG;AACH,IAAA,IAAI,CAAC,MAAiB,EAAA;AACpB,QAAA,IAAI,CAAC,MAAM,GAAG,MAAM;;QAEpB,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC;IAC9C;IAEA,QAAQ,GAAA;AACN,QAAA,OAAO,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE;IACrC;AAEA;;;AAGG;IACH,MAAM,KAAK,CAAC,WAAoB,EAAA;AAC9B,QAAA,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;AAChB,YAAA,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC;QACxC;QACA,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,KAAK;QAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE;AAC3C,QAAA,MAAM,IAAI,GAAG,CAAA,EAAG,QAAQ,CAAA,sBAAA,EAAyB,cAAc,EAAE;AACjE,QAAA,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,EAAC,EAAC,QAAQ,EAAC,KAAK,EAAC,CAAC;AACtE,QAAA,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,YAAY;AAChD,QAAA,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,CAAC;AACzC,QAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;AACnD,QAAA,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,QAAQ;IACjC;AAEA;;AAEG;AACH,IAAA,MAAM,MAAM,GAAA;AACV,QAAA,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;AAChB,YAAA,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC;QACxC;;AAEA,QAAA,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE;AAC9B,QAAA,IAAI,CAAC,aAAa,GAAG,IAAI;AACzB,QAAA,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC;QAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE;AAC3C,QAAA,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA,OAAA,CAAS,EAAC,IAAI,EAAC,EAAC,QAAQ,EAAC,IAAI,EAAC,CAAC;;AAErE,QAAA,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;AACnB,QAAA,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,GAAC,YAAY,GAAC,IAAI,CAAC,MAAM,CAAC,QAAQ;IACnF;AAEA;;;AAGG;AACH,IAAA,MAAM,cAAc,GAAA;AAClB,QAAA,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;AAChB,YAAA,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC;QACxC;AAEA,QAAA,MAAM,MAAM,GAAG,gBAAgB,EAAE;;AAGjC,QAAA,IAAI,MAAM,CAAC,KAAK,EAAE;AAChB,YAAA,MAAM,IAAI,KAAK,CAAC,CAAA,qBAAA,EAAwB,MAAM,CAAC,KAAK,CAAA,GAAA,EAAM,MAAM,CAAC,iBAAiB,IAAI,EAAE,CAAA,CAAE,CAAC;QAC7F;;AAGA,QAAA,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;AAChB,YAAA,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC;QACjD;QAEE,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,KAAK;QAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE;AAC3C,QAAA,MAAM,QAAQ,GAAG,CAAA,EAAG,QAAQ,CAAA,mBAAA,EAAsB,cAAc,CAAA,EAAG,gBAAgB,CAAC,MAAM,CAAC,CAAA,CAAE;QAC7F,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAC;AACvD,YAAA,OAAO,EAAE;AACP,gBAAA,cAAc,EAAE;AACjB,aAAA;AACD,YAAA,QAAQ,EAAE;AACX,SAAA,CAAC;;AAEF,QAAA,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC;AAClB,QAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;AACrD,QAAA,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,eAAe,CAAC,IAAE,aAAa,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;AAC3G,QAAA,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ;AAC9B,QAAA,IAAG,MAAM,CAAC,KAAK,EAAC;AACd,YAAA,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG;QAC7C;AACA,QAAA,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,GAAG;IAE9B;AAEA,IAAA,MAAM,SAAS,GAAA;AACb,QAAA,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;AAChB,YAAA,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC;QACxC;QACA,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE;AAC3C,QAAA,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAA,WAAA,CAAa,EAAC,EAAC,QAAQ,EAAC,IAAI,EAAC,CAAC;AACzF,QAAA,IAAG,aAAa,CAAC,MAAM,KAAG,GAAG,EAAC;AAC5B,YAAA,MAAM,IAAI,CAAC,MAAM,EAAE;QACrB;AACA,QAAA,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI;IAEhC;AACA;;;AAGG;IACF,WAAW,GAAA;QACV,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IACrC;AAKA;;;;AAIG;IACH,MAAM,OAAO,CAAC,IAAuB,EAAA;AACnC,QAAA,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE;AAC3B,YAAA,OAAO,KAAK;QACd;QAEA,MAAM,QAAQ,GAAY,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;AACtD,QAAA,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,IAAE,EAAE;AAEpC,QAAA,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;;AAEvB,YAAA,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC9C;;AAGA,QAAA,OAAO,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;IACjC;AAEA;;;;AAIG;IACH,MAAM,WAAW,CAAC,KAAe,EAAA;AAC/B,QAAA,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE;AAC3B,YAAA,OAAO,KAAK;QACd;QAEA,MAAM,QAAQ,GAAY,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;AACtD,QAAA,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,IAAE,EAAE;;AAEpC,QAAA,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAChD;AAEA;;;;AAIG;IACH,MAAM,aAAa,CAAC,UAA6B,EAAA;AAC/C,QAAA,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE;AAC3B,YAAA,OAAO,KAAK;QACd;QAEA,MAAM,QAAQ,GAAY,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;AACtD,QAAA,MAAM,WAAW,GAAG,QAAQ,CAAC,WAAW,IAAE,EAAE;AAE5C,QAAA,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;;AAE7B,YAAA,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACtD;;AAGA,QAAA,OAAO,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC;IACzC;AAEA;;;;AAIG;IACH,MAAM,iBAAiB,CAAC,WAAqB,EAAA;AAC3C,QAAA,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE;AAC3B,YAAA,OAAO,KAAK;QACd;QAEA,MAAM,QAAQ,GAAY,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;AACtD,QAAA,MAAM,eAAe,GAAG,QAAQ,CAAC,WAAW,IAAE,EAAE;;AAGhD,QAAA,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,IAAI,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5D;AAEA;;;AAGG;IACH,eAAe,GAAA;;QAEb,OAAO,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE;IACvC;AAEA;;;;AAIG;IACH,EAAE,CAAC,KAAgB,EAAE,QAAkB,EAAA;QACrC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC;IAC1C;AAEA;;;;AAIG;IACH,GAAG,CAAC,KAAgB,EAAE,QAAkB,EAAA;QACtC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,OAAO,KAAK,QAAQ,CAAC;IAC/F;AAEA;;;;AAIG;IACK,IAAI,CAAC,KAAgB,EAAE,IAAU,EAAA;QACvC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,IAAG;AAC1C,YAAA,IAAI;gBACF,OAAO,CAAC,IAAI,CAAC;YACf;YAAE,OAAO,KAAK,EAAE;gBACd,OAAO,CAAC,KAAK,CAAC,CAAA,SAAA,EAAY,KAAK,CAAA,eAAA,CAAiB,EAAE,KAAK,CAAC;YAC1D;AACF,QAAA,CAAC,CAAC;IACJ;AAEA;;;AAGG;IACH,UAAU,GAAA;QACR,OAAO,aAAa,EAAE;IACxB;AACD;;ACvRD;;;AAGG;AAIH;;AAEG;MACU,OAAO,CAAA;AAIlB;;;;AAIG;AACH,IAAA,WAAA,CAAY,WAAA,GAA2B,cAAc,EAAE,MAAA,GAAiB,gBAAgB,EAAA;AACtF,QAAA,IAAI,CAAC,WAAW,GAAG,WAAW;AAC9B,QAAA,IAAI,CAAC,MAAM,GAAG,MAAM;IACtB;AAEA;;;;;AAKG;AACH,IAAA,GAAG,CAAC,GAAW,EAAE,KAAU,EAAE,OAAgF,EAAA;AAC3G,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG;AACjC,QAAA,MAAM,WAAW,GAAG,OAAO,KAAK,KAAK,QAAQ,GAAG,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;AAE7E,QAAA,QAAQ,IAAI,CAAC,WAAW;AACtB,YAAA,KAAK,cAAc;AACjB,gBAAA,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,WAAW,CAAC;gBAC1C;AACF,YAAA,KAAK,gBAAgB;AACnB,gBAAA,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,WAAW,CAAC;gBAC5C;AACF,YAAA,KAAK,QAAQ;gBACX,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC;gBAC7C;;IAEN;AAEA;;;;AAIG;AACH,IAAA,GAAG,CAAC,GAAW,EAAA;AACb,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG;AACjC,QAAA,IAAI,KAAU;AAEd,QAAA,QAAQ,IAAI,CAAC,WAAW;AACtB,YAAA,KAAK,cAAc;AACjB,gBAAA,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC;gBACrC;AACF,YAAA,KAAK,gBAAgB;AACnB,gBAAA,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC;gBACvC;AACF,YAAA,KAAK,QAAQ;AACX,gBAAA,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;gBAC/B;AACF,YAAA;gBACE,KAAK,GAAG,IAAI;;AAGhB,QAAA,IAAI,KAAK,KAAK,IAAI,EAAE;AAClB,YAAA,OAAO,IAAI;QACb;;AAGA,QAAA,IAAI;AACF,YAAA,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAC1B;QAAE,OAAO,CAAC,EAAE;;AAEV,YAAA,OAAO,KAAK;QACd;IACF;AAEA;;;AAGG;AACH,IAAA,MAAM,CAAC,GAAW,EAAA;AAChB,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG;AAEjC,QAAA,QAAQ,IAAI,CAAC,WAAW;AACtB,YAAA,KAAK,cAAc;AACjB,gBAAA,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC;gBAChC;AACF,YAAA,KAAK,gBAAgB;AACnB,gBAAA,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC;gBAClC;AACF,YAAA,KAAK,QAAQ;AACX,gBAAA,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC;gBAC1B;;IAEN;AAEA;;AAEG;IACH,KAAK,GAAA;AACH,QAAA,QAAQ,IAAI,CAAC,WAAW;AACtB,YAAA,KAAK,cAAc;gBACjB,IAAI,CAAC,iBAAiB,EAAE;gBACxB;AACF,YAAA,KAAK,gBAAgB;gBACnB,IAAI,CAAC,mBAAmB,EAAE;gBAC1B;AACF,YAAA,KAAK,QAAQ;gBACX,IAAI,CAAC,WAAW,EAAE;gBAClB;;IAEN;AAEA;;;AAGG;IACH,WAAW,GAAA;AACT,QAAA,IAAI;AACF,YAAA,QAAQ,IAAI,CAAC,WAAW;AACtB,gBAAA,KAAK,cAAc;AACjB,oBAAA,OAAO,IAAI,CAAC,uBAAuB,EAAE;AACvC,gBAAA,KAAK,gBAAgB;AACnB,oBAAA,OAAO,IAAI,CAAC,yBAAyB,EAAE;AACzC,gBAAA,KAAK,QAAQ;AACX,oBAAA,OAAO,OAAO,QAAQ,KAAK,WAAW;AACxC,gBAAA;AACE,oBAAA,OAAO,KAAK;;QAElB;QAAE,OAAO,CAAC,EAAE;AACV,YAAA,OAAO,KAAK;QACd;IACF;;AAIA;;AAEG;IACK,eAAe,CAAC,GAAW,EAAE,KAAa,EAAA;AAChD,QAAA,IAAI,IAAI,CAAC,uBAAuB,EAAE,EAAE;AAClC,YAAA,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC;QAClC;IACF;AAEA;;AAEG;AACK,IAAA,eAAe,CAAC,GAAW,EAAA;AACjC,QAAA,IAAI,IAAI,CAAC,uBAAuB,EAAE,EAAE;AAClC,YAAA,OAAO,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC;QAClC;AACA,QAAA,OAAO,IAAI;IACb;AAEA;;AAEG;AACK,IAAA,kBAAkB,CAAC,GAAW,EAAA;AACpC,QAAA,IAAI,IAAI,CAAC,uBAAuB,EAAE,EAAE;AAClC,YAAA,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC;QAC9B;IACF;AAEA;;AAEG;IACK,iBAAiB,GAAA;AACvB,QAAA,IAAI,IAAI,CAAC,uBAAuB,EAAE,EAAE;AAClC,YAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;gBAC5C,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC/B,IAAI,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;AACtC,oBAAA,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC;oBAC5B,CAAC,EAAE,CAAC;gBACN;YACF;QACF;IACF;AAEA;;AAEG;IACK,uBAAuB,GAAA;AAC7B,QAAA,IAAI,OAAO,YAAY,KAAK,WAAW,EAAE;AACvC,YAAA,OAAO,KAAK;QACd;AACA,QAAA,IAAI;YACF,MAAM,OAAO,GAAG,kBAAkB;AAClC,YAAA,YAAY,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC;AACtC,YAAA,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC;AAChC,YAAA,OAAO,IAAI;QACb;QAAE,OAAO,CAAC,EAAE;AACV,YAAA,OAAO,KAAK;QACd;IACF;;AAIA;;AAEG;IACK,iBAAiB,CAAC,GAAW,EAAE,KAAa,EAAA;AAClD,QAAA,IAAI,IAAI,CAAC,yBAAyB,EAAE,EAAE;AACpC,YAAA,cAAc,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC;QACpC;IACF;AAEA;;AAEG;AACK,IAAA,iBAAiB,CAAC,GAAW,EAAA;AACnC,QAAA,IAAI,IAAI,CAAC,yBAAyB,EAAE,EAAE;AACpC,YAAA,OAAO,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC;QACpC;AACA,QAAA,OAAO,IAAI;IACb;AAEA;;AAEG;AACK,IAAA,oBAAoB,CAAC,GAAW,EAAA;AACtC,QAAA,IAAI,IAAI,CAAC,yBAAyB,EAAE,EAAE;AACpC,YAAA,cAAc,CAAC,UAAU,CAAC,GAAG,CAAC;QAChC;IACF;AAEA;;AAEG;IACK,mBAAmB,GAAA;AACzB,QAAA,IAAI,IAAI,CAAC,yBAAyB,EAAE,EAAE;AACpC,YAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;gBAC9C,MAAM,GAAG,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;gBACjC,IAAI,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;AACtC,oBAAA,cAAc,CAAC,UAAU,CAAC,GAAG,CAAC;oBAC9B,CAAC,EAAE,CAAC;gBACN;YACF;QACF;IACF;AAEA;;AAEG;IACK,yBAAyB,GAAA;AAC/B,QAAA,IAAI,OAAO,cAAc,KAAK,WAAW,EAAE;AACzC,YAAA,OAAO,KAAK;QACd;AACA,QAAA,IAAI;YACF,MAAM,OAAO,GAAG,kBAAkB;AAClC,YAAA,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC;AACxC,YAAA,cAAc,CAAC,UAAU,CAAC,OAAO,CAAC;AAClC,YAAA,OAAO,IAAI;QACb;QAAE,OAAO,CAAC,EAAE;AACV,YAAA,OAAO,KAAK;QACd;IACF;;AAIA;;AAEG;AACK,IAAA,SAAS,CACf,GAAW,EACX,KAAa,EACb,OAAgF,EAAA;AAEhF,QAAA,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE;YACnC;QACF;QAEA,IAAI,YAAY,GAAG,CAAA,EAAG,GAAG,CAAA,CAAA,EAAI,kBAAkB,CAAC,KAAK,CAAC,CAAA,CAAE;QAExD,IAAI,OAAO,EAAE;;AAEX,YAAA,IAAI,OAAO,CAAC,OAAO,EAAE;AACnB,gBAAA,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE;AACvB,gBAAA,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;AACrD,gBAAA,YAAY,IAAI,CAAA,UAAA,EAAa,IAAI,CAAC,WAAW,EAAE,EAAE;YACnD;;AAGA,YAAA,IAAI,OAAO,CAAC,IAAI,EAAE;AAChB,gBAAA,YAAY,IAAI,CAAA,OAAA,EAAU,OAAO,CAAC,IAAI,EAAE;YAC1C;;AAGA,YAAA,IAAI,OAAO,CAAC,MAAM,EAAE;AAClB,gBAAA,YAAY,IAAI,CAAA,SAAA,EAAY,OAAO,CAAC,MAAM,EAAE;YAC9C;;AAGA,YAAA,IAAI,OAAO,CAAC,MAAM,EAAE;gBAClB,YAAY,IAAI,UAAU;YAC5B;QACF;AAEA,QAAA,QAAQ,CAAC,MAAM,GAAG,YAAY;IAChC;AAEA;;AAEG;AACK,IAAA,SAAS,CAAC,GAAW,EAAA;AAC3B,QAAA,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE;AACnC,YAAA,OAAO,IAAI;QACb;AAEA,QAAA,MAAM,IAAI,GAAG,CAAA,EAAG,GAAG,GAAG;QACtB,MAAM,aAAa,GAAG,kBAAkB,CAAC,QAAQ,CAAC,MAAM,CAAC;QACzD,MAAM,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC;AAEnC,QAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AAClC,YAAA,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACb,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;AAC1B,gBAAA,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;YACpB;YACA,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;AACzB,gBAAA,OAAO,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;YAC3C;QACF;AAEA,QAAA,OAAO,IAAI;IACb;AAEA;;AAEG;AACK,IAAA,YAAY,CAAC,GAAW,EAAA;AAC9B,QAAA,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC1C;AAEA;;AAEG;IACK,WAAW,GAAA;AACjB,QAAA,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE;YACnC;QACF;QAEA,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;AAC1C,QAAA,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE;YAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC;YACjC,MAAM,GAAG,GAAG,KAAK,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE;YACvE,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;AAC/B,gBAAA,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;YACxB;QACF;IACF;AACD;;ACrWD;;;AAGG;AA0BH;;AAEG;MACU,WAAW,CAAA;AAGtB;;;AAGG;AACH,IAAA,WAAA,CAAY,IAAU,EAAA;AACpB,QAAA,IAAI,CAAC,IAAI,GAAG,IAAI;IAClB;AAEA;;;;AAIG;IACH,MAAM,KAAK,CAAC,OAA2B,EAAA;QACrC,MAAM,EAAE,YAAY,GAAG,IAAI,EAAE,mBAAmB,GAAG,EAAE,EAAE,GAAG,OAAO;;QAGjE,IAAI,YAAY,EAAE;;YAEhB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE;;gBAEhC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC;AACpC,gBAAA,OAAO,KAAK;YACd;;AAGA,YAAA,IAAI,mBAAmB,CAAC,MAAM,GAAG,CAAC,EAAE;;AAElC,gBAAA,MAAM,eAAe,GAAG,CAAC,EAAE,CAAC;;AAG5B,gBAAA,MAAM,aAAa,GAAG,mBAAmB,CAAC,KAAK,CAAC,UAAU,IACxD,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC,CACrC;gBAED,IAAI,CAAC,aAAa,EAAE;;AAElB,oBAAA,IAAI,OAAO,CAAC,uBAAuB,EAAE;wBACnC,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,uBAAuB;oBACxD;AACA,oBAAA,OAAO,KAAK;gBACd;YACF;QACF;AAEA,QAAA,OAAO,IAAI;IACb;AAEA;;;AAGG;IACH,cAAc,GAAA;QACZ,OAAO,OAAO,EAAO,EAAE,IAAS,EAAE,IAAS,KAAI;;;YAE7C,MAAM,OAAO,GAAuB,CAAA,CAAA,EAAA,GAAA,EAAE,CAAC,IAAI,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,MAAA,GAAA,EAAA,CAAE,IAAI,KAAI,EAAE;AAEvD,YAAA,IAAI;gBACF,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;gBACzC,IAAI,OAAO,EAAE;AACX,oBAAA,IAAI,EAAE;gBACR;YACF;YAAE,OAAO,KAAK,EAAE;AACd,gBAAA,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,KAAK,CAAC;gBAC1C,IAAI,CAAC,KAAK,CAAC;YACb;AACF,QAAA,CAAC;IACH;AAEA;;;;AAIG;IACH,MAAM,aAAa,CAAC,WAA8B,EAAA;QAChD,IAAI,CAAC,WAAW,EAAE;AAChB,YAAA,OAAO,IAAI;QACb;AAEA,QAAA,MAAM,mBAAmB,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,GAAG,CAAC,WAAW,CAAC;;QAGpF,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE;AAChC,YAAA,OAAO,KAAK;QACd;;AAGA,QAAA,MAAM,eAAe,GAAG,CAAC,EAAE,CAAC;;AAG5B,QAAA,OAAO,mBAAmB,CAAC,KAAK,CAAC,UAAU,IACzC,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC,CACrC;IACH;AACD;;ACjID;;;AAGG;AAqBH;;AAEG;MACU,SAAS,CAAA;AAIpB;;;AAGG;AACH,IAAA,WAAA,CAAY,OAAgB,EAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;IAC/C;AAEA;;;;AAIG;IACH,OAAO,CAAC,GAAQ,EAAE,OAAyB,EAAA;QACzC,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,cAAc,EAAE,GAAG,OAAO;;AAGvD,QAAA,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;;QAGtB,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,WAAW;QAEhD,IAAI,MAAM,EAAE;;;AAGV,YAAA,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAA,EAAG,UAAU,CAAA,CAAE,CAAC,GAAG,IAAI,CAAC,IAAI;AACxD,YAAA,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;;YAG9C,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC;YAClC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;;YAG/B,GAAG,CAAC,KAAK,CAAC;gBACR,YAAY,GAAA;;AAEV,oBAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;AACxB,wBAAA,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM;;wBAEnC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC;oBACtD;gBACF;AACD,aAAA,CAAC;QACJ;aAAO;;;YAGL,GAAG,CAAC,SAAS,CAAC,CAAA,EAAG,UAAU,CAAA,CAAE,CAAC,GAAG,IAAI,CAAC,IAAI;YAC1C,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;;YAGhC,GAAG,CAAC,KAAK,CAAC;gBACR,YAAY,GAAA;;AAEV,oBAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;AACxB,wBAAA,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM;;wBAEnC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC;oBACtD;gBACF;AACD,aAAA,CAAC;QACJ;IACF;AAEA;;;AAGG;IACH,OAAO,GAAA;QACL,OAAO,IAAI,CAAC,IAAI;IAClB;AAEA;;;AAGG;IACH,cAAc,GAAA;QACZ,OAAO,IAAI,CAAC,WAAW;IACzB;AACD;AAED;;;;AAIG;AACG,SAAU,eAAe,CAAC,WAA0D,EAAA;AACxF,IAAA,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,WAAW,CAAC;AACxC,IAAA,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC;AAC/B;;ACxHA;;;AAGG;AAEH;AA2BA;;AAEG;AACH,MAAM,cAAc,GAAG,IAAIA,OAAW,EAAE;AACxC,MAAM,WAAW,GAAG,IAAIC,IAAQ,CAAC,cAAc,CAAC;AAEhD;;AAEG;AACI,MAAM,eAAe,GAAoB;AAC9C,IAAA,IAAI,EAAE,CAAC,MAAiB,KAAI;AAC1B,QAAA,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC;IAC1B,CAAC;IACD,QAAQ,EAAE,MAAK;AACb,QAAA,OAAO,WAAW,CAAC,QAAQ,EAAE;IAC/B,CAAC;AACD,IAAA,KAAK,EAAE,CAAC,WAAoB,KAAI;AAC9B,QAAA,OAAO,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC;IACvC,CAAC;IACD,MAAM,EAAE,MAAK;AACX,QAAA,OAAO,WAAW,CAAC,MAAM,EAAE;IAC7B,CAAC;IACD,cAAc,EAAE,MAAK;AACnB,QAAA,OAAO,WAAW,CAAC,cAAc,EAAE;IACrC,CAAC;IACD,SAAS,EAAE,MAAK;AACd,QAAA,OAAO,WAAW,CAAC,SAAS,EAAE;IAChC,CAAC;IACD,WAAW,EAAE,MAAK;AAChB,QAAA,OAAO,WAAW,CAAC,WAAW,EAAE;IAClC,CAAC;IACD,eAAe,EAAE,MAAK;AACpB,QAAA,OAAO,WAAW,CAAC,eAAe,EAAE;IACtC,CAAC;AACD,IAAA,OAAO,EAAE,CAAC,IAAuB,KAAI;AACnC,QAAA,OAAO,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;IAClC,CAAC;AACD,IAAA,WAAW,EAAE,CAAC,KAAe,KAAI;AAC/B,QAAA,OAAO,WAAW,CAAC,WAAW,CAAC,KAAK,CAAC;IACvC,CAAC;AACD,IAAA,aAAa,EAAE,CAAC,UAA6B,KAAI;AAC/C,QAAA,OAAO,WAAW,CAAC,aAAa,CAAC,UAAU,CAAC;IAC9C,CAAC;AACD,IAAA,iBAAiB,EAAE,CAAC,WAAqB,KAAI;AAC3C,QAAA,OAAO,WAAW,CAAC,iBAAiB,CAAC,WAAW,CAAC;IACnD,CAAC;AACD,IAAA,EAAE,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAI;QACtB,OAAO,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC;IACxC,CAAC;AACD,IAAA,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAI;QACvB,OAAO,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzC,CAAC;IACD,UAAU,EAAE,MAAK;AACf,QAAA,OAAO,WAAW,CAAC,UAAU,EAAE;IACjC;;AAMF;AACO,MAAM,OAAO,GAAG;;;;"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/index.js b/sdk/frontend/oauth2-login-sdk/dist/index.js new file mode 100644 index 0000000..cf417b3 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/index.js @@ -0,0 +1,1211 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +/** + * Token管理模块 + * 负责Token的存储、获取、刷新和过期处理 + */ +/** + * Token管理类 + */ +class TokenManager { + /** + * 构造函数 + * @param storage 存储实例 + * @param httpClient HTTP客户端实例 + */ + constructor(storage) { + this.storage = storage; + } + /** + * 存储Token信息 + * @param tokenInfo Token信息 + */ + saveToken(tokenInfo) { + this.storage.set('token', tokenInfo); + } + /** + * 获取Token信息 + * @returns TokenInfo | null Token信息 + */ + getToken() { + return this.storage.get('token'); + } + /** + * 清除Token信息 + */ + clearToken() { + this.storage.remove('token'); + } +} + +/** + * HTTP客户端 + * 用于与后端API进行通信 + */ +/** + * HTTP错误类型 + */ +class HttpError extends Error { + /** + * 构造函数 + * @param message 错误信息 + * @param status 状态码 + * @param statusText 状态文本 + * @param data 错误数据 + */ + constructor(message, status, statusText, data) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.statusText = statusText; + this.data = data; + } +} +/** + * HTTP客户端类 + */ +class HttpClient { + /** + * 构造函数 + * @param logout + * @param tokenGetter Token获取函数 + */ + constructor(tokenGetter) { + this.tokenGetter = tokenGetter; + } + /** + * 设置Token获取函数 + * @param tokenGetter Token获取函数 + */ + setTokenGetter(tokenGetter) { + this.tokenGetter = tokenGetter; + } + /** + * 设置租户ID + * @param tenantId 租户ID + */ + setTenantId(tenantId) { + this.tenantId = tenantId; + } + /** + * 发送HTTP请求 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async request(options) { + const { method, url, headers = {}, body, needAuth = true } = options; + // 构建请求头 + const requestHeaders = { + 'Content-Type': 'application/json', + ...headers + }; + // 添加认证头 + const addAuthHeader = () => { + if (needAuth && this.tokenGetter) { + const token = this.tokenGetter(); + if (token) { + requestHeaders.Authorization = `${token}`; + } + } + }; + // 添加租户ID头 + if (this.tenantId) { + requestHeaders['tenant-id'] = this.tenantId; + } + addAuthHeader(); + // 构建请求配置 + const fetchOptions = { + method, + headers: requestHeaders, + credentials: 'include' // 包含cookie + }; + // 添加请求体 + if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { + fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body); + } + try { + // 发送请求 + const response = await fetch(url, fetchOptions); + const responseData = await this.parseResponse(response); + // 检查响应状态 + if (!response.ok) { + // 如果是401错误,尝试刷新Token并重试 + if (response.status === 401) { + return { + status: response.status, + statusText: response.statusText, + data: '', + headers: this.parseHeaders(response.headers) + }; + } + // 其他错误,直接抛出 + const errorMsg = this.getErrorMessage(responseData); + throw new HttpError(errorMsg, response.status, response.statusText, responseData); + } + // 处理成功响应的业务逻辑 + return this.handleResponse(response, responseData); + } + catch (error) { + if (error instanceof HttpError) { + throw error; + } + // 网络错误或其他错误 + throw new HttpError(error instanceof Error ? error.message : 'Network Error', 0, 'Network Error', null); + } + } + /** + * 处理响应数据 + * @param response 响应对象 + * @param responseData 响应数据 + * @returns HttpResponse 处理后的响应 + */ + handleResponse(response, responseData) { + // 检查是否为业务响应结构 + if (this.isBusinessResponse(responseData)) { + // 业务响应结构:{ code, msg, data } + const { code, msg, data } = responseData; + // 检查业务状态码 + if (code !== 0 && code !== 200 && code !== '0' && code !== '200') { + // 业务错误,抛出HttpError + throw new HttpError(msg || `Business Error: ${code}`, response.status, response.statusText, responseData); + } + // 业务成功,返回data字段作为实际数据 + return { + status: response.status, + statusText: response.statusText, + data: data, + headers: this.parseHeaders(response.headers) + }; + } + // 非业务响应结构,直接返回原始数据 + return { + status: response.status, + statusText: response.statusText, + data: responseData, + headers: this.parseHeaders(response.headers) + }; + } + /** + * 检查是否为业务响应结构 + * @param responseData 响应数据 + * @returns boolean 是否为业务响应结构 + */ + isBusinessResponse(responseData) { + return typeof responseData === 'object' && + responseData !== null && + ('code' in responseData) && + ('msg' in responseData) && + ('data' in responseData); + } + /** + * 获取错误信息 + * @param responseData 响应数据 + * @returns string 错误信息 + */ + getErrorMessage(responseData) { + // 如果是业务响应结构 + if (this.isBusinessResponse(responseData)) { + return responseData.msg || `Business Error: ${responseData.code}`; + } + // 其他错误结构 + return responseData.message || responseData.error || `HTTP Error`; + } + /** + * GET请求 + * @param url 请求URL + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async get(url, options) { + return this.request({ + method: 'GET', + url, + ...options + }); + } + /** + * POST请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async post(url, body, options) { + return this.request({ + method: 'POST', + url, + body, + ...options + }); + } + /** + * PUT请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async put(url, body, options) { + return this.request({ + method: 'PUT', + url, + body, + ...options + }); + } + /** + * DELETE请求 + * @param url 请求URL + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async delete(url, options) { + return this.request({ + method: 'DELETE', + url, + ...options + }); + } + /** + * PATCH请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async patch(url, body, options) { + return this.request({ + method: 'PATCH', + url, + body, + ...options + }); + } + /** + * 解析响应体 + * @param response 响应对象 + * @returns Promise 解析后的响应体 + */ + async parseResponse(response) { + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('application/json')) { + return response.json(); + } + else if (contentType.includes('text/')) { + return response.text(); + } + else { + return response.blob(); + } + } + /** + * 解析响应头 + * @param headers 响应头对象 + * @returns Record 解析后的响应头 + */ + parseHeaders(headers) { + const result = {}; + headers.forEach((value, key) => { + result[key] = value; + }); + return result; + } +} + +/** + * URL处理工具 + * 用于生成授权URL、解析URL参数等功能 + */ +/** + * 生成随机字符串 + * @param length 字符串长度,默认32位 + * @returns 随机字符串 + */ +function generateRandomString(length = 32) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} +/** + * 解析URL查询参数 + * @param url URL字符串,默认为当前URL + * @returns 查询参数对象 + */ +function parseQueryParams(url = window.location.href) { + const params = {}; + const queryString = url.split('?')[1]; + if (!queryString) { + return params; + } + const pairs = queryString.split('&'); + for (const pair of pairs) { + const [key, value] = pair.split('='); + if (key) { + params[decodeURIComponent(key)] = decodeURIComponent(value || ''); + } + } + return params; +} +/** + * 构建URL查询参数 + * @param params 查询参数对象 + * @returns 查询参数字符串 + */ +function buildQueryParams(params) { + const pairs = []; + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); + } + } + return pairs.length ? `?${pairs.join('&')}` : ''; +} +/** + * 生成OAuth2授权URL + * @param authorizationEndpoint 授权端点URL + * @param clientId 客户端ID + * @param redirectUri 重定向URL + * @param options 可选参数 + * @returns 授权URL + */ +function generateAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, options) { + const { responseType = 'code', scope, state = generateRandomString(32), ...extraParams } = options || {}; + const params = { + client_id: clientId, + redirect_uri: redirectUri, + response_type: responseType, + state, + ...(scope ? { scope } : {}), + ...extraParams + }; + const queryString = buildQueryParams(params); + return `${authorizationEndpoint}${queryString}`; +} +/** + * 检查当前URL是否为授权回调 + * @param url URL字符串,默认为当前URL + * @returns 是否为授权回调 + */ +function isCallbackUrl(url = window.location.href) { + const params = parseQueryParams(url); + return !!params.code || !!params.error; +} + +/** + * 认证核心逻辑 + * 实现OAuth2授权码模式的完整流程 + */ +/** + * 认证核心类 + */ +class Auth { + /** + * 构造函数 + * @param storage 存储实例 + */ + constructor(storage) { + this.config = null; + this.eventHandlers = { + login: [], + logout: [], + tokenExpired: [] + }; + this.userInfoCache = null; + this.storage = storage; + // 先创建HttpClient,初始时tokenManager为undefined + this.httpClient = new HttpClient(() => this.tokenManager.getToken() || null); + // 然后创建TokenManager + this.tokenManager = new TokenManager(storage); + } + /** + * 初始化SDK配置 + * @param config SDK配置选项 + */ + init(config) { + this.config = config; + // 设置租户ID到HTTP客户端 + this.httpClient.setTenantId(config.tenantId); + } + getToken() { + return this.tokenManager.getToken(); + } + /** + * 触发登录流程 + * @param redirectUri 可选的重定向URL,覆盖初始化时的配置 + */ + async login(redirectUri) { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const registrationId = this.config.registrationId || 'idp'; + const basepath = this.config.basepath || ''; + const path = `${basepath}/oauth2/authorization/${registrationId}`; + const tokenResponse = await this.httpClient.get(path, { needAuth: false }); + const redirect = tokenResponse.data.redirect_url; + const params = parseQueryParams(redirect); + this.storage.set(params.state, window.location.href); + window.location.href = redirect; + } + /** + * 退出登录 + */ + async logout() { + if (!this.config) { + throw new Error('SDK not initialized'); + } + // 清除本地存储的Token和用户信息 + this.tokenManager.clearToken(); + this.userInfoCache = null; + this.storage.remove('userInfo'); + const basepath = this.config.basepath || ''; + await this.httpClient.post(`${basepath}/logout`, null, { needAuth: true }); + // 触发退出事件 + this.emit('logout'); + window.location.href = this.config.idpLogoutUrl + '?redirect=' + this.config.homePage; + } + /** + * 处理授权回调 + * @returns Promise 用户信息 + */ + async handleCallback() { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const params = parseQueryParams(); + // 检查是否有错误 + if (params.error) { + throw new Error(`Authorization error: ${params.error} - ${params.error_description || ''}`); + } + // 检查是否有授权码 + if (!params.code) { + throw new Error('Authorization code not found'); + } + const registrationId = this.config.registrationId || 'idp'; + const basepath = this.config.basepath || ''; + const callback = `${basepath}/login/oauth2/code/${registrationId}${buildQueryParams(params)}`; + const tokenResponse = await this.httpClient.get(callback, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + needAuth: false + }); + // 触发登录事件 + this.emit('login'); + this.storage.set('userInfo', tokenResponse.data.data); + this.tokenManager.saveToken(tokenResponse.headers['authorization'] || tokenResponse.headers['Authorization']); + let url = this.config.homePage; + if (params.state) { + url = this.storage.get(params.state) || url; + } + window.location.href = url; + } + async getRoutes() { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const basepath = this.config.basepath || ''; + const tokenResponse = await this.httpClient.get(`${basepath}/idp/routes`, { needAuth: true }); + if (tokenResponse.status === 401) { + await this.logout(); + } + return tokenResponse.data.data; + } + /** + * 获取用户信息 + * @returns UserInfo 用户信息 + */ + getUserInfo() { + return this.storage.get("userInfo"); + } + /** + * 检查用户是否有指定角色 + * @param role 角色编码或角色编码列表 + * @returns Promise 是否有指定角色 + */ + async hasRole(role) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const roleCodes = userInfo.roles || []; + if (Array.isArray(role)) { + // 检查是否有任一角色 + return role.some(r => roleCodes.includes(r)); + } + // 检查是否有单个角色 + return roleCodes.includes(role); + } + /** + * 检查用户是否有所有指定角色 + * @param roles 角色编码列表 + * @returns Promise 是否有所有指定角色 + */ + async hasAllRoles(roles) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const roleCodes = userInfo.roles || []; + // 检查是否有所有角色 + return roles.every(r => roleCodes.includes(r)); + } + /** + * 检查用户是否有指定权限 + * @param permission 权限标识或权限标识列表 + * @returns Promise 是否有指定权限 + */ + async hasPermission(permission) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const permissions = userInfo.permissions || []; + if (Array.isArray(permission)) { + // 检查是否有任一权限 + return permission.some(p => permissions.includes(p)); + } + // 检查是否有单个权限 + return permissions.includes(permission); + } + /** + * 检查用户是否有所有指定权限 + * @param permissions 权限标识列表 + * @returns Promise 是否有所有指定权限 + */ + async hasAllPermissions(permissions) { + if (!this.isAuthenticated()) { + return false; + } + const userInfo = this.storage.get("userInfo"); + const userPermissions = userInfo.permissions || []; + // 检查是否有所有权限 + return permissions.every(p => userPermissions.includes(p)); + } + /** + * 检查用户是否已认证 + * @returns boolean 是否已认证 + */ + isAuthenticated() { + // 检查Token是否存在且未过期 + return !!this.tokenManager.getToken(); + } + /** + * 事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + on(event, callback) { + this.eventHandlers[event].push(callback); + } + /** + * 移除事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + off(event, callback) { + this.eventHandlers[event] = this.eventHandlers[event].filter(handler => handler !== callback); + } + /** + * 触发事件 + * @param event 事件类型 + * @param data 事件数据 + */ + emit(event, data) { + this.eventHandlers[event].forEach(handler => { + try { + handler(data); + } + catch (error) { + console.error(`Error in ${event} event handler:`, error); + } + }); + } + /** + * 检查当前URL是否为授权回调 + * @returns boolean 是否为授权回调 + */ + isCallback() { + return isCallbackUrl(); + } +} + +/** + * 存储工具类 + * 支持localStorage、sessionStorage和cookie三种存储方式 + */ +/** + * 存储工具类 + */ +class Storage { + /** + * 构造函数 + * @param storageType 存储类型 + * @param prefix 存储前缀,默认'unified_login_' + */ + constructor(storageType = 'localStorage', prefix = 'unified_login_') { + this.storageType = storageType; + this.prefix = prefix; + } + /** + * 设置存储项 + * @param key 存储键 + * @param value 存储值 + * @param options 可选参数,cookie存储时使用 + */ + set(key, value, options) { + const fullKey = this.prefix + key; + const stringValue = typeof value === 'string' ? value : JSON.stringify(value); + switch (this.storageType) { + case 'localStorage': + this.setLocalStorage(fullKey, stringValue); + break; + case 'sessionStorage': + this.setSessionStorage(fullKey, stringValue); + break; + case 'cookie': + this.setCookie(fullKey, stringValue, options); + break; + } + } + /** + * 获取存储项 + * @param key 存储键 + * @returns 存储值 + */ + get(key) { + const fullKey = this.prefix + key; + let value; + switch (this.storageType) { + case 'localStorage': + value = this.getLocalStorage(fullKey); + break; + case 'sessionStorage': + value = this.getSessionStorage(fullKey); + break; + case 'cookie': + value = this.getCookie(fullKey); + break; + default: + value = null; + } + if (value === null) { + return null; + } + // 尝试解析JSON + try { + return JSON.parse(value); + } + catch (e) { + // 如果不是JSON,直接返回字符串 + return value; + } + } + /** + * 移除存储项 + * @param key 存储键 + */ + remove(key) { + const fullKey = this.prefix + key; + switch (this.storageType) { + case 'localStorage': + this.removeLocalStorage(fullKey); + break; + case 'sessionStorage': + this.removeSessionStorage(fullKey); + break; + case 'cookie': + this.removeCookie(fullKey); + break; + } + } + /** + * 清空所有存储项 + */ + clear() { + switch (this.storageType) { + case 'localStorage': + this.clearLocalStorage(); + break; + case 'sessionStorage': + this.clearSessionStorage(); + break; + case 'cookie': + this.clearCookie(); + break; + } + } + /** + * 检查存储类型是否可用 + * @returns boolean 是否可用 + */ + isAvailable() { + try { + switch (this.storageType) { + case 'localStorage': + return this.isLocalStorageAvailable(); + case 'sessionStorage': + return this.isSessionStorageAvailable(); + case 'cookie': + return typeof document !== 'undefined'; + default: + return false; + } + } + catch (e) { + return false; + } + } + // ------------------------ localStorage 操作 ------------------------ + /** + * 设置localStorage + */ + setLocalStorage(key, value) { + if (this.isLocalStorageAvailable()) { + localStorage.setItem(key, value); + } + } + /** + * 获取localStorage + */ + getLocalStorage(key) { + if (this.isLocalStorageAvailable()) { + return localStorage.getItem(key); + } + return null; + } + /** + * 移除localStorage + */ + removeLocalStorage(key) { + if (this.isLocalStorageAvailable()) { + localStorage.removeItem(key); + } + } + /** + * 清空localStorage中所有带前缀的项 + */ + clearLocalStorage() { + if (this.isLocalStorageAvailable()) { + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(this.prefix)) { + localStorage.removeItem(key); + i--; // 索引调整 + } + } + } + } + /** + * 检查localStorage是否可用 + */ + isLocalStorageAvailable() { + if (typeof localStorage === 'undefined') { + return false; + } + try { + const testKey = '__storage_test__'; + localStorage.setItem(testKey, testKey); + localStorage.removeItem(testKey); + return true; + } + catch (e) { + return false; + } + } + // ------------------------ sessionStorage 操作 ------------------------ + /** + * 设置sessionStorage + */ + setSessionStorage(key, value) { + if (this.isSessionStorageAvailable()) { + sessionStorage.setItem(key, value); + } + } + /** + * 获取sessionStorage + */ + getSessionStorage(key) { + if (this.isSessionStorageAvailable()) { + return sessionStorage.getItem(key); + } + return null; + } + /** + * 移除sessionStorage + */ + removeSessionStorage(key) { + if (this.isSessionStorageAvailable()) { + sessionStorage.removeItem(key); + } + } + /** + * 清空sessionStorage中所有带前缀的项 + */ + clearSessionStorage() { + if (this.isSessionStorageAvailable()) { + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + if (key && key.startsWith(this.prefix)) { + sessionStorage.removeItem(key); + i--; // 索引调整 + } + } + } + } + /** + * 检查sessionStorage是否可用 + */ + isSessionStorageAvailable() { + if (typeof sessionStorage === 'undefined') { + return false; + } + try { + const testKey = '__storage_test__'; + sessionStorage.setItem(testKey, testKey); + sessionStorage.removeItem(testKey); + return true; + } + catch (e) { + return false; + } + } + // ------------------------ cookie 操作 ------------------------ + /** + * 设置cookie + */ + setCookie(key, value, options) { + if (typeof document === 'undefined') { + return; + } + let cookieString = `${key}=${encodeURIComponent(value)}`; + if (options) { + // 设置过期时间(秒) + if (options.expires) { + const date = new Date(); + date.setTime(date.getTime() + options.expires * 1000); + cookieString += `; expires=${date.toUTCString()}`; + } + // 设置路径 + if (options.path) { + cookieString += `; path=${options.path}`; + } + // 设置域名 + if (options.domain) { + cookieString += `; domain=${options.domain}`; + } + // 设置secure + if (options.secure) { + cookieString += '; secure'; + } + } + document.cookie = cookieString; + } + /** + * 获取cookie + */ + getCookie(key) { + if (typeof document === 'undefined') { + return null; + } + const name = `${key}=`; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return null; + } + /** + * 移除cookie + */ + removeCookie(key) { + this.setCookie(key, '', { expires: -1 }); + } + /** + * 清空所有带前缀的cookie + */ + clearCookie() { + if (typeof document === 'undefined') { + return; + } + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const eqPos = cookie.indexOf('='); + const key = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + if (key.startsWith(this.prefix)) { + this.removeCookie(key); + } + } + } +} + +/** + * 路由守卫模块 + * 提供基于权限的路由拦截和未登录自动跳转登录页功能 + */ +/** + * 路由守卫类 + */ +class RouterGuard { + /** + * 构造函数 + * @param auth 认证实例 + */ + constructor(auth) { + this.auth = auth; + } + /** + * 检查路由权限 + * @param options 路由守卫选项 + * @returns Promise 是否通过权限检查 + */ + async check(options) { + const { requiresAuth = true, requiredPermissions = [] } = options; + // 检查是否需要登录 + if (requiresAuth) { + // 检查是否已认证 + if (!this.auth.isAuthenticated()) { + // 未认证,跳转到登录页 + this.auth.login(options.redirectUri); + return false; + } + // 检查是否需要权限 + if (requiredPermissions.length > 0) { + // 获取用户权限 + const userPermissions = ['']; + // 检查是否拥有所有需要的权限 + const hasPermission = requiredPermissions.every(permission => userPermissions.includes(permission)); + if (!hasPermission) { + // 权限不足,跳转到权限不足页 + if (options.unauthorizedRedirectUri) { + window.location.href = options.unauthorizedRedirectUri; + } + return false; + } + } + } + return true; + } + /** + * 创建Vue路由守卫 + * @returns 路由守卫函数 + */ + createVueGuard() { + return async (to, from, next) => { + var _a; + // 从路由元信息中获取守卫选项 + const options = ((_a = to.meta) === null || _a === void 0 ? void 0 : _a.auth) || {}; + try { + const allowed = await this.check(options); + if (allowed) { + next(); + } + } + catch (error) { + console.error('Route guard error:', error); + next(false); + } + }; + } + /** + * 检查当前用户是否有权限访问资源 + * @param permissions 需要的权限列表 + * @returns Promise 是否拥有权限 + */ + async hasPermission(permissions) { + if (!permissions) { + return true; + } + const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions]; + // 检查是否已认证 + if (!this.auth.isAuthenticated()) { + return false; + } + // 获取用户权限 + const userPermissions = ['']; + // 检查是否拥有所有需要的权限 + return requiredPermissions.every(permission => userPermissions.includes(permission)); + } +} + +/** + * Vue插件模块 + * 提供Vue应用中使用统一登录SDK的能力 + */ +/** + * Vue插件类 + */ +class VuePlugin { + /** + * 构造函数 + * @param storage 存储实例 + */ + constructor(storage) { + this.auth = new Auth(storage); + this.routerGuard = new RouterGuard(this.auth); + } + /** + * 安装Vue插件 + * @param app Vue构造函数或Vue 3应用实例 + * @param options 插件选项 + */ + install(app, options) { + const { config, pluginName = 'unifiedLogin' } = options; + // 初始化SDK + this.auth.init(config); + // 判断是Vue 2还是Vue 3 + const isVue3 = typeof app.config !== 'undefined'; + if (isVue3) { + // Vue 3 + // 在全局属性上挂载SDK实例 + app.config.globalProperties[`${pluginName}`] = this.auth; + app.config.globalProperties.$auth = this.auth; // 兼容简写 + // 提供Vue组件内的注入 + app.provide(pluginName, this.auth); + app.provide('auth', this.auth); // 兼容简写 + // 处理路由守卫 + app.mixin({ + beforeCreate() { + // 如果是根组件,添加路由守卫 + if (this.$options.router) { + const router = this.$options.router; + // 添加全局前置守卫 + router.beforeEach(this.routerGuard.createVueGuard()); + } + } + }); + } + else { + // Vue 2 + // 在Vue实例上挂载SDK实例 + app.prototype[`${pluginName}`] = this.auth; + app.prototype.$auth = this.auth; // 兼容简写 + // 全局混入 + app.mixin({ + beforeCreate() { + // 如果是根组件,添加路由守卫 + if (this.$options.router) { + const router = this.$options.router; + // 添加全局前置守卫 + router.beforeEach(this.routerGuard.createVueGuard()); + } + } + }); + } + } + /** + * 获取认证实例 + * @returns Auth 认证实例 + */ + getAuth() { + return this.auth; + } + /** + * 获取路由守卫实例 + * @returns RouterGuard 路由守卫实例 + */ + getRouterGuard() { + return this.routerGuard; + } +} +/** + * 创建Vue插件实例 + * @param storageType 存储类型 + * @returns VuePlugin Vue插件实例 + */ +function createVuePlugin(storageType) { + const storage = new Storage(storageType); + return new VuePlugin(storage); +} + +/** + * 统一登录SDK入口文件 + * 支持OAuth2授权码模式,提供完整的Token管理和用户信息管理功能 + */ +// 导出核心类和功能 +/** + * 默认SDK实例 + */ +const defaultStorage = new Storage(); +const defaultAuth = new Auth(defaultStorage); +/** + * 默认导出的SDK实例 + */ +const unifiedLoginSDK = { + init: (config) => { + defaultAuth.init(config); + }, + getToken: () => { + return defaultAuth.getToken(); + }, + login: (redirectUri) => { + return defaultAuth.login(redirectUri); + }, + logout: () => { + return defaultAuth.logout(); + }, + handleCallback: () => { + return defaultAuth.handleCallback(); + }, + getRoutes: () => { + return defaultAuth.getRoutes(); + }, + getUserInfo: () => { + return defaultAuth.getUserInfo(); + }, + isAuthenticated: () => { + return defaultAuth.isAuthenticated(); + }, + hasRole: (role) => { + return defaultAuth.hasRole(role); + }, + hasAllRoles: (roles) => { + return defaultAuth.hasAllRoles(roles); + }, + hasPermission: (permission) => { + return defaultAuth.hasPermission(permission); + }, + hasAllPermissions: (permissions) => { + return defaultAuth.hasAllPermissions(permissions); + }, + on: (event, callback) => { + return defaultAuth.on(event, callback); + }, + off: (event, callback) => { + return defaultAuth.off(event, callback); + }, + isCallback: () => { + return defaultAuth.isCallback(); + } +}; +// 版本信息 +const version = '1.0.0'; + +exports.Auth = Auth; +exports.HttpClient = HttpClient; +exports.HttpError = HttpError; +exports.RouterGuard = RouterGuard; +exports.Storage = Storage; +exports.TokenManager = TokenManager; +exports.VuePlugin = VuePlugin; +exports.buildQueryParams = buildQueryParams; +exports.createVuePlugin = createVuePlugin; +exports.default = unifiedLoginSDK; +exports.generateAuthorizationUrl = generateAuthorizationUrl; +exports.generateRandomString = generateRandomString; +exports.isCallbackUrl = isCallbackUrl; +exports.parseQueryParams = parseQueryParams; +exports.unifiedLoginSDK = unifiedLoginSDK; +exports.version = version; +//# sourceMappingURL=index.js.map diff --git a/sdk/frontend/oauth2-login-sdk/dist/index.js.map b/sdk/frontend/oauth2-login-sdk/dist/index.js.map new file mode 100644 index 0000000..3e91b4f --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sources":["../src/core/token.ts","../src/core/http.ts","../src/utils/url.ts","../src/core/auth.ts","../src/utils/storage.ts","../src/guards/router.ts","../src/plugins/vue.ts","../src/index.ts"],"sourcesContent":["/**\r\n * Token管理模块\r\n * 负责Token的存储、获取、刷新和过期处理\r\n */\r\n\r\nimport { Storage } from '../utils/storage';\r\n\r\n/**\r\n * Token管理类\r\n */\r\nexport class TokenManager {\r\n private storage: Storage;\r\n\r\n /**\r\n * 构造函数\r\n * @param storage 存储实例\r\n * @param httpClient HTTP客户端实例\r\n */\r\n constructor(storage: Storage) {\r\n this.storage = storage;\r\n }\r\n\r\n /**\r\n * 存储Token信息\r\n * @param tokenInfo Token信息\r\n */\r\n saveToken(tokenInfo: string): void {\r\n this.storage.set('token', tokenInfo);\r\n }\r\n\r\n /**\r\n * 获取Token信息\r\n * @returns TokenInfo | null Token信息\r\n */\r\n getToken(): string | null {\r\n return this.storage.get('token');\r\n }\r\n\r\n /**\r\n * 清除Token信息\r\n */\r\n clearToken(): void {\r\n this.storage.remove('token');\r\n }\r\n}\r\n","/**\r\n * HTTP客户端\r\n * 用于与后端API进行通信\r\n */\r\n\r\n/**\r\n * HTTP请求方法类型\r\n */\r\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\r\n\r\n/**\r\n * HTTP请求选项\r\n */\r\nexport interface HttpRequestOptions {\r\n /** 请求方法 */\r\n method: HttpMethod;\r\n /** 请求URL */\r\n url: string;\r\n /** 请求头 */\r\n headers?: Record;\r\n /** 请求体 */\r\n body?: any;\r\n /** 是否需要认证 */\r\n needAuth?: boolean;\r\n}\r\n\r\n/**\r\n * HTTP响应类型\r\n */\r\nexport interface HttpResponse {\r\n /** 状态码 */\r\n status: number;\r\n /** 状态文本 */\r\n statusText: string;\r\n /** 响应体 */\r\n data: T;\r\n /** 响应头 */\r\n headers: Record;\r\n}\r\n\r\n/**\r\n * HTTP错误类型\r\n */\r\nexport class HttpError extends Error {\r\n /** 状态码 */\r\n public status: number;\r\n /** 状态文本 */\r\n public statusText: string;\r\n /** 错误数据 */\r\n public data: any;\r\n\r\n /**\r\n * 构造函数\r\n * @param message 错误信息\r\n * @param status 状态码\r\n * @param statusText 状态文本\r\n * @param data 错误数据\r\n */\r\n constructor(message: string, status: number, statusText: string, data: any) {\r\n super(message);\r\n this.name = 'HttpError';\r\n this.status = status;\r\n this.statusText = statusText;\r\n this.data = data;\r\n }\r\n}\r\n\r\n/**\r\n * HTTP客户端类\r\n */\r\nexport class HttpClient {\r\n private tokenGetter?: () => string | null;\r\n private tenantId?: string;\r\n\r\n /**\r\n * 构造函数\r\n * @param logout\r\n * @param tokenGetter Token获取函数\r\n */\r\n constructor(tokenGetter?: () => string | null) {\r\n this.tokenGetter = tokenGetter;\r\n\r\n }\r\n\r\n /**\r\n * 设置Token获取函数\r\n * @param tokenGetter Token获取函数\r\n */\r\n setTokenGetter(tokenGetter: () => string | null): void {\r\n this.tokenGetter = tokenGetter;\r\n }\r\n\r\n /**\r\n * 设置租户ID\r\n * @param tenantId 租户ID\r\n */\r\n setTenantId(tenantId?: string): void {\r\n this.tenantId = tenantId;\r\n }\r\n\r\n /**\r\n * 发送HTTP请求\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async request(options: HttpRequestOptions): Promise> {\r\n const {\r\n method,\r\n url,\r\n headers = {},\r\n body,\r\n needAuth = true\r\n } = options;\r\n\r\n // 构建请求头\r\n const requestHeaders: Record = {\r\n 'Content-Type': 'application/json',\r\n ...headers\r\n };\r\n\r\n // 添加认证头\r\n const addAuthHeader = () => {\r\n if (needAuth && this.tokenGetter) {\r\n const token = this.tokenGetter();\r\n if (token) {\r\n requestHeaders.Authorization = `${token}`;\r\n }\r\n }\r\n };\r\n\r\n // 添加租户ID头\r\n if (this.tenantId) {\r\n requestHeaders['tenant-id'] = this.tenantId;\r\n }\r\n\r\n addAuthHeader();\r\n\r\n // 构建请求配置\r\n const fetchOptions: RequestInit = {\r\n method,\r\n headers: requestHeaders,\r\n credentials: 'include' // 包含cookie\r\n };\r\n\r\n // 添加请求体\r\n if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {\r\n fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);\r\n }\r\n\r\n try {\r\n // 发送请求\r\n const response = await fetch(url, fetchOptions);\r\n const responseData = await this.parseResponse(response);\r\n\r\n // 检查响应状态\r\n if (!response.ok) {\r\n // 如果是401错误,尝试刷新Token并重试\r\n if (response.status === 401) {\r\n return {\r\n status: response.status,\r\n statusText: response.statusText,\r\n data: '' as T,\r\n headers: this.parseHeaders(response.headers)\r\n }\r\n }\r\n \r\n // 其他错误,直接抛出\r\n const errorMsg = this.getErrorMessage(responseData);\r\n throw new HttpError(\r\n errorMsg,\r\n response.status,\r\n response.statusText,\r\n responseData\r\n );\r\n }\r\n\r\n // 处理成功响应的业务逻辑\r\n return this.handleResponse(response, responseData);\r\n } catch (error) {\r\n if (error instanceof HttpError) {\r\n throw error;\r\n }\r\n\r\n // 网络错误或其他错误\r\n throw new HttpError(\r\n error instanceof Error ? error.message : 'Network Error',\r\n 0,\r\n 'Network Error',\r\n null\r\n );\r\n }\r\n }\r\n \r\n\r\n\r\n /**\r\n * 处理响应数据\r\n * @param response 响应对象\r\n * @param responseData 响应数据\r\n * @returns HttpResponse 处理后的响应\r\n */\r\n private handleResponse(response: Response, responseData: any): HttpResponse {\r\n // 检查是否为业务响应结构\r\n if (this.isBusinessResponse(responseData)) {\r\n // 业务响应结构:{ code, msg, data }\r\n const { code, msg, data } = responseData;\r\n \r\n // 检查业务状态码\r\n if (code !== 0 && code !== 200 && code !== '0' && code !== '200') {\r\n // 业务错误,抛出HttpError\r\n throw new HttpError(\r\n msg || `Business Error: ${code}`,\r\n response.status,\r\n response.statusText,\r\n responseData\r\n );\r\n }\r\n \r\n // 业务成功,返回data字段作为实际数据\r\n return {\r\n status: response.status,\r\n statusText: response.statusText,\r\n data: data as T,\r\n headers: this.parseHeaders(response.headers)\r\n };\r\n }\r\n \r\n // 非业务响应结构,直接返回原始数据\r\n return {\r\n status: response.status,\r\n statusText: response.statusText,\r\n data: responseData as T,\r\n headers: this.parseHeaders(response.headers)\r\n };\r\n }\r\n\r\n /**\r\n * 检查是否为业务响应结构\r\n * @param responseData 响应数据\r\n * @returns boolean 是否为业务响应结构\r\n */\r\n private isBusinessResponse(responseData: any): boolean {\r\n return typeof responseData === 'object' && \r\n responseData !== null && \r\n ('code' in responseData) && \r\n ('msg' in responseData) && \r\n ('data' in responseData);\r\n }\r\n\r\n /**\r\n * 获取错误信息\r\n * @param responseData 响应数据\r\n * @returns string 错误信息\r\n */\r\n private getErrorMessage(responseData: any): string {\r\n // 如果是业务响应结构\r\n if (this.isBusinessResponse(responseData)) {\r\n return responseData.msg || `Business Error: ${responseData.code}`;\r\n }\r\n \r\n // 其他错误结构\r\n return responseData.message || responseData.error || `HTTP Error`;\r\n }\r\n\r\n /**\r\n * GET请求\r\n * @param url 请求URL\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async get(url: string, options?: Omit): Promise> {\r\n return this.request({\r\n method: 'GET',\r\n url,\r\n ...options\r\n });\r\n }\r\n\r\n /**\r\n * POST请求\r\n * @param url 请求URL\r\n * @param body 请求体\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async post(url: string, body?: any, options?: Omit): Promise> {\r\n return this.request({\r\n method: 'POST',\r\n url,\r\n body,\r\n ...options\r\n });\r\n }\r\n\r\n /**\r\n * PUT请求\r\n * @param url 请求URL\r\n * @param body 请求体\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async put(url: string, body?: any, options?: Omit): Promise> {\r\n return this.request({\r\n method: 'PUT',\r\n url,\r\n body,\r\n ...options\r\n });\r\n }\r\n\r\n /**\r\n * DELETE请求\r\n * @param url 请求URL\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async delete(url: string, options?: Omit): Promise> {\r\n return this.request({\r\n method: 'DELETE',\r\n url,\r\n ...options\r\n });\r\n }\r\n\r\n /**\r\n * PATCH请求\r\n * @param url 请求URL\r\n * @param body 请求体\r\n * @param options 请求选项\r\n * @returns Promise> 响应结果\r\n */\r\n async patch(url: string, body?: any, options?: Omit): Promise> {\r\n return this.request({\r\n method: 'PATCH',\r\n url,\r\n body,\r\n ...options\r\n });\r\n }\r\n\r\n /**\r\n * 解析响应体\r\n * @param response 响应对象\r\n * @returns Promise 解析后的响应体\r\n */\r\n private async parseResponse(response: Response): Promise {\r\n const contentType = response.headers.get('content-type') || '';\r\n \r\n if (contentType.includes('application/json')) {\r\n return response.json();\r\n } else if (contentType.includes('text/')) {\r\n return response.text();\r\n } else {\r\n return response.blob();\r\n }\r\n }\r\n\r\n /**\r\n * 解析响应头\r\n * @param headers 响应头对象\r\n * @returns Record 解析后的响应头\r\n */\r\n private parseHeaders(headers: Headers): Record {\r\n const result: Record = {};\r\n headers.forEach((value, key) => {\r\n result[key] = value;\r\n });\r\n return result;\r\n }\r\n}\r\n","/**\r\n * URL处理工具\r\n * 用于生成授权URL、解析URL参数等功能\r\n */\r\n\r\n/**\r\n * 生成随机字符串\r\n * @param length 字符串长度,默认32位\r\n * @returns 随机字符串\r\n */\r\nexport function generateRandomString(length: number = 32): string {\r\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r\n let result = '';\r\n for (let i = 0; i < length; i++) {\r\n result += chars.charAt(Math.floor(Math.random() * chars.length));\r\n }\r\n return result;\r\n}\r\n\r\n/**\r\n * 解析URL查询参数\r\n * @param url URL字符串,默认为当前URL\r\n * @returns 查询参数对象\r\n */\r\nexport function parseQueryParams(url: string = window.location.href): Record {\r\n const params: Record = {};\r\n const queryString = url.split('?')[1];\r\n if (!queryString) {\r\n return params;\r\n }\r\n\r\n const pairs = queryString.split('&');\r\n for (const pair of pairs) {\r\n const [key, value] = pair.split('=');\r\n if (key) {\r\n params[decodeURIComponent(key)] = decodeURIComponent(value || '');\r\n }\r\n }\r\n\r\n return params;\r\n}\r\n\r\n/**\r\n * 构建URL查询参数\r\n * @param params 查询参数对象\r\n * @returns 查询参数字符串\r\n */\r\nexport function buildQueryParams(params: Record): string {\r\n const pairs: string[] = [];\r\n for (const [key, value] of Object.entries(params)) {\r\n if (value !== undefined && value !== null) {\r\n pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);\r\n }\r\n }\r\n return pairs.length ? `?${pairs.join('&')}` : '';\r\n}\r\n\r\n/**\r\n * 生成OAuth2授权URL\r\n * @param authorizationEndpoint 授权端点URL\r\n * @param clientId 客户端ID\r\n * @param redirectUri 重定向URL\r\n * @param options 可选参数\r\n * @returns 授权URL\r\n */\r\nexport function generateAuthorizationUrl(\r\n authorizationEndpoint: string,\r\n clientId: string,\r\n redirectUri: string,\r\n options?: {\r\n responseType?: string;\r\n scope?: string;\r\n state?: string;\r\n [key: string]: any;\r\n }\r\n): string {\r\n const {\r\n responseType = 'code',\r\n scope,\r\n state = generateRandomString(32),\r\n ...extraParams\r\n } = options || {};\r\n\r\n const params = {\r\n client_id: clientId,\r\n redirect_uri: redirectUri,\r\n response_type: responseType,\r\n state,\r\n ...(scope ? { scope } : {}),\r\n ...extraParams\r\n };\r\n\r\n const queryString = buildQueryParams(params);\r\n return `${authorizationEndpoint}${queryString}`;\r\n}\r\n\r\n/**\r\n * 检查当前URL是否为授权回调\r\n * @param url URL字符串,默认为当前URL\r\n * @returns 是否为授权回调\r\n */\r\nexport function isCallbackUrl(url: string = window.location.href): boolean {\r\n const params = parseQueryParams(url);\r\n return !!params.code || !!params.error;\r\n}\r\n\r\n/**\r\n * 获取当前URL的路径名\r\n * @param url URL字符串,默认为当前URL\r\n * @returns 路径名\r\n */\r\nexport function getPathname(url: string = window.location.href): string {\r\n const urlObj = new URL(url);\r\n return urlObj.pathname;\r\n}\r\n\r\n/**\r\n * 获取当前URL的主机名\r\n * @param url URL字符串,默认为当前URL\r\n * @returns 主机名\r\n */\r\nexport function getHostname(url: string = window.location.href): string {\r\n const urlObj = new URL(url);\r\n return urlObj.hostname;\r\n}\r\n","/**\r\n * 认证核心逻辑\r\n * 实现OAuth2授权码模式的完整流程\r\n */\r\n\r\nimport {EventType, RouterInfo, SDKConfig, UserInfo} from '../types';\r\nimport {TokenManager} from './token';\r\nimport {HttpClient} from './http';\r\nimport {Storage} from '../utils/storage';\r\nimport {buildQueryParams, isCallbackUrl, parseQueryParams} from '../utils/url';\r\n\r\n/**\r\n * 认证核心类\r\n */\r\nexport class Auth {\r\n private config: SDKConfig | null = null;\r\n private tokenManager: TokenManager;\r\n private httpClient: HttpClient;\r\n private storage: Storage;\r\n private eventHandlers: Record = {\r\n login: [],\r\n logout: [],\r\n tokenExpired: []\r\n };\r\n private userInfoCache: UserInfo | null = null;\r\n\r\n /**\r\n * 构造函数\r\n * @param storage 存储实例\r\n */\r\n constructor(storage: Storage) {\r\n this.storage = storage;\r\n // 先创建HttpClient,初始时tokenManager为undefined\r\n this.httpClient = new HttpClient(() => this.tokenManager.getToken() || null);\r\n // 然后创建TokenManager\r\n this.tokenManager = new TokenManager(storage);\r\n }\r\n\r\n /**\r\n * 初始化SDK配置\r\n * @param config SDK配置选项\r\n */\r\n init(config: SDKConfig): void {\r\n this.config = config;\r\n // 设置租户ID到HTTP客户端\r\n this.httpClient.setTenantId(config.tenantId);\r\n }\r\n\r\n getToken():string | null{\r\n return this.tokenManager.getToken()\r\n }\r\n\r\n /**\r\n * 触发登录流程\r\n * @param redirectUri 可选的重定向URL,覆盖初始化时的配置\r\n */\r\n async login(redirectUri?: string): Promise {\r\n if (!this.config) {\r\n throw new Error('SDK not initialized');\r\n }\r\n const registrationId = this.config.registrationId || 'idp'\r\n const basepath = this.config.basepath || ''\r\n const path = `${basepath}/oauth2/authorization/${registrationId}`\r\n const tokenResponse = await this.httpClient.get(path,{needAuth:false})\r\n const redirect = tokenResponse.data.redirect_url\r\n const params = parseQueryParams(redirect)\r\n this.storage.set(params.state,window.location.href)\r\n window.location.href = redirect\r\n }\r\n\r\n /**\r\n * 退出登录\r\n */\r\n async logout(): Promise {\r\n if (!this.config) {\r\n throw new Error('SDK not initialized');\r\n }\r\n // 清除本地存储的Token和用户信息\r\n this.tokenManager.clearToken();\r\n this.userInfoCache = null;\r\n this.storage.remove('userInfo');\r\n const basepath = this.config.basepath || ''\r\n await this.httpClient.post(`${basepath}/logout`,null,{needAuth:true})\r\n // 触发退出事件\r\n this.emit('logout');\r\n window.location.href = this.config.idpLogoutUrl+'?redirect='+this.config.homePage;\r\n }\r\n\r\n /**\r\n * 处理授权回调\r\n * @returns Promise 用户信息\r\n */\r\n async handleCallback(): Promise {\r\n if (!this.config) {\r\n throw new Error('SDK not initialized');\r\n }\r\n\r\n const params = parseQueryParams();\r\n \r\n // 检查是否有错误\r\n if (params.error) {\r\n throw new Error(`Authorization error: ${params.error} - ${params.error_description || ''}`);\r\n }\r\n\r\n // 检查是否有授权码\r\n if (!params.code) {\r\n throw new Error('Authorization code not found');\r\n }\r\n\r\n const registrationId = this.config.registrationId || 'idp'\r\n const basepath = this.config.basepath || ''\r\n const callback = `${basepath}/login/oauth2/code/${registrationId}${buildQueryParams(params)}`\r\n const tokenResponse = await this.httpClient.get(callback,{\r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded'\r\n },\r\n needAuth: false\r\n })\r\n // 触发登录事件\r\n this.emit('login');\r\n this.storage.set('userInfo', tokenResponse.data.data);\r\n this.tokenManager.saveToken(tokenResponse.headers['authorization']||tokenResponse.headers['Authorization'])\r\n let url = this.config.homePage\r\n if(params.state){\r\n url = this.storage.get(params.state) || url;\r\n }\r\n window.location.href = url;\r\n\r\n }\r\n\r\n async getRoutes(): Promise {\r\n if (!this.config) {\r\n throw new Error('SDK not initialized');\r\n }\r\n const basepath = this.config.basepath || ''\r\n const tokenResponse = await this.httpClient.get(`${basepath}/idp/routes`,{needAuth:true})\r\n if(tokenResponse.status===401){\r\n await this.logout()\r\n }\r\n return tokenResponse.data.data\r\n\r\n }\r\n /**\r\n * 获取用户信息\r\n * @returns UserInfo 用户信息\r\n */\r\n getUserInfo(): UserInfo {\r\n return this.storage.get(\"userInfo\");\r\n }\r\n\r\n\r\n\r\n\r\n /**\r\n * 检查用户是否有指定角色\r\n * @param role 角色编码或角色编码列表\r\n * @returns Promise 是否有指定角色\r\n */\r\n async hasRole(role: string | string[]): Promise {\r\n if (!this.isAuthenticated()) {\r\n return false;\r\n }\r\n\r\n const userInfo:UserInfo = this.storage.get(\"userInfo\");\r\n const roleCodes = userInfo.roles||[];\r\n\r\n if (Array.isArray(role)) {\r\n // 检查是否有任一角色\r\n return role.some(r => roleCodes.includes(r));\r\n }\r\n\r\n // 检查是否有单个角色\r\n return roleCodes.includes(role);\r\n }\r\n\r\n /**\r\n * 检查用户是否有所有指定角色\r\n * @param roles 角色编码列表\r\n * @returns Promise 是否有所有指定角色\r\n */\r\n async hasAllRoles(roles: string[]): Promise {\r\n if (!this.isAuthenticated()) {\r\n return false;\r\n }\r\n\r\n const userInfo:UserInfo = this.storage.get(\"userInfo\");\r\n const roleCodes = userInfo.roles||[];\r\n // 检查是否有所有角色\r\n return roles.every(r => roleCodes.includes(r));\r\n }\r\n\r\n /**\r\n * 检查用户是否有指定权限\r\n * @param permission 权限标识或权限标识列表\r\n * @returns Promise 是否有指定权限\r\n */\r\n async hasPermission(permission: string | string[]): Promise {\r\n if (!this.isAuthenticated()) {\r\n return false;\r\n }\r\n\r\n const userInfo:UserInfo = this.storage.get(\"userInfo\");\r\n const permissions = userInfo.permissions||[];\r\n\r\n if (Array.isArray(permission)) {\r\n // 检查是否有任一权限\r\n return permission.some(p => permissions.includes(p));\r\n }\r\n\r\n // 检查是否有单个权限\r\n return permissions.includes(permission);\r\n }\r\n\r\n /**\r\n * 检查用户是否有所有指定权限\r\n * @param permissions 权限标识列表\r\n * @returns Promise 是否有所有指定权限\r\n */\r\n async hasAllPermissions(permissions: string[]): Promise {\r\n if (!this.isAuthenticated()) {\r\n return false;\r\n }\r\n\r\n const userInfo:UserInfo = this.storage.get(\"userInfo\");\r\n const userPermissions = userInfo.permissions||[];\r\n\r\n // 检查是否有所有权限\r\n return permissions.every(p => userPermissions.includes(p));\r\n }\r\n\r\n /**\r\n * 检查用户是否已认证\r\n * @returns boolean 是否已认证\r\n */\r\n isAuthenticated(): boolean {\r\n // 检查Token是否存在且未过期\r\n return !!this.tokenManager.getToken();\r\n }\r\n\r\n /**\r\n * 事件监听\r\n * @param event 事件类型\r\n * @param callback 回调函数\r\n */\r\n on(event: EventType, callback: Function): void {\r\n this.eventHandlers[event].push(callback);\r\n }\r\n\r\n /**\r\n * 移除事件监听\r\n * @param event 事件类型\r\n * @param callback 回调函数\r\n */\r\n off(event: EventType, callback: Function): void {\r\n this.eventHandlers[event] = this.eventHandlers[event].filter(handler => handler !== callback);\r\n }\r\n\r\n /**\r\n * 触发事件\r\n * @param event 事件类型\r\n * @param data 事件数据\r\n */\r\n private emit(event: EventType, data?: any): void {\r\n this.eventHandlers[event].forEach(handler => {\r\n try {\r\n handler(data);\r\n } catch (error) {\r\n console.error(`Error in ${event} event handler:`, error);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * 检查当前URL是否为授权回调\r\n * @returns boolean 是否为授权回调\r\n */\r\n isCallback(): boolean {\r\n return isCallbackUrl();\r\n }\r\n}\r\n","/**\r\n * 存储工具类\r\n * 支持localStorage、sessionStorage和cookie三种存储方式\r\n */\r\n\r\ntype StorageType = 'localStorage' | 'sessionStorage' | 'cookie';\r\n\r\n/**\r\n * 存储工具类\r\n */\r\nexport class Storage {\r\n private storageType: StorageType;\r\n private prefix: string;\r\n\r\n /**\r\n * 构造函数\r\n * @param storageType 存储类型\r\n * @param prefix 存储前缀,默认'unified_login_'\r\n */\r\n constructor(storageType: StorageType = 'localStorage', prefix: string = 'unified_login_') {\r\n this.storageType = storageType;\r\n this.prefix = prefix;\r\n }\r\n\r\n /**\r\n * 设置存储项\r\n * @param key 存储键\r\n * @param value 存储值\r\n * @param options 可选参数,cookie存储时使用\r\n */\r\n set(key: string, value: any, options?: { expires?: number; path?: string; domain?: string; secure?: boolean }): void {\r\n const fullKey = this.prefix + key;\r\n const stringValue = typeof value === 'string' ? value : JSON.stringify(value);\r\n\r\n switch (this.storageType) {\r\n case 'localStorage':\r\n this.setLocalStorage(fullKey, stringValue);\r\n break;\r\n case 'sessionStorage':\r\n this.setSessionStorage(fullKey, stringValue);\r\n break;\r\n case 'cookie':\r\n this.setCookie(fullKey, stringValue, options);\r\n break;\r\n }\r\n }\r\n\r\n /**\r\n * 获取存储项\r\n * @param key 存储键\r\n * @returns 存储值\r\n */\r\n get(key: string): any {\r\n const fullKey = this.prefix + key;\r\n let value: any;\r\n\r\n switch (this.storageType) {\r\n case 'localStorage':\r\n value = this.getLocalStorage(fullKey);\r\n break;\r\n case 'sessionStorage':\r\n value = this.getSessionStorage(fullKey);\r\n break;\r\n case 'cookie':\r\n value = this.getCookie(fullKey);\r\n break;\r\n default:\r\n value = null;\r\n }\r\n\r\n if (value === null) {\r\n return null;\r\n }\r\n\r\n // 尝试解析JSON\r\n try {\r\n return JSON.parse(value);\r\n } catch (e) {\r\n // 如果不是JSON,直接返回字符串\r\n return value;\r\n }\r\n }\r\n\r\n /**\r\n * 移除存储项\r\n * @param key 存储键\r\n */\r\n remove(key: string): void {\r\n const fullKey = this.prefix + key;\r\n\r\n switch (this.storageType) {\r\n case 'localStorage':\r\n this.removeLocalStorage(fullKey);\r\n break;\r\n case 'sessionStorage':\r\n this.removeSessionStorage(fullKey);\r\n break;\r\n case 'cookie':\r\n this.removeCookie(fullKey);\r\n break;\r\n }\r\n }\r\n\r\n /**\r\n * 清空所有存储项\r\n */\r\n clear(): void {\r\n switch (this.storageType) {\r\n case 'localStorage':\r\n this.clearLocalStorage();\r\n break;\r\n case 'sessionStorage':\r\n this.clearSessionStorage();\r\n break;\r\n case 'cookie':\r\n this.clearCookie();\r\n break;\r\n }\r\n }\r\n\r\n /**\r\n * 检查存储类型是否可用\r\n * @returns boolean 是否可用\r\n */\r\n isAvailable(): boolean {\r\n try {\r\n switch (this.storageType) {\r\n case 'localStorage':\r\n return this.isLocalStorageAvailable();\r\n case 'sessionStorage':\r\n return this.isSessionStorageAvailable();\r\n case 'cookie':\r\n return typeof document !== 'undefined';\r\n default:\r\n return false;\r\n }\r\n } catch (e) {\r\n return false;\r\n }\r\n }\r\n\r\n // ------------------------ localStorage 操作 ------------------------\r\n\r\n /**\r\n * 设置localStorage\r\n */\r\n private setLocalStorage(key: string, value: string): void {\r\n if (this.isLocalStorageAvailable()) {\r\n localStorage.setItem(key, value);\r\n }\r\n }\r\n\r\n /**\r\n * 获取localStorage\r\n */\r\n private getLocalStorage(key: string): string | null {\r\n if (this.isLocalStorageAvailable()) {\r\n return localStorage.getItem(key);\r\n }\r\n return null;\r\n }\r\n\r\n /**\r\n * 移除localStorage\r\n */\r\n private removeLocalStorage(key: string): void {\r\n if (this.isLocalStorageAvailable()) {\r\n localStorage.removeItem(key);\r\n }\r\n }\r\n\r\n /**\r\n * 清空localStorage中所有带前缀的项\r\n */\r\n private clearLocalStorage(): void {\r\n if (this.isLocalStorageAvailable()) {\r\n for (let i = 0; i < localStorage.length; i++) {\r\n const key = localStorage.key(i);\r\n if (key && key.startsWith(this.prefix)) {\r\n localStorage.removeItem(key);\r\n i--; // 索引调整\r\n }\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * 检查localStorage是否可用\r\n */\r\n private isLocalStorageAvailable(): boolean {\r\n if (typeof localStorage === 'undefined') {\r\n return false;\r\n }\r\n try {\r\n const testKey = '__storage_test__';\r\n localStorage.setItem(testKey, testKey);\r\n localStorage.removeItem(testKey);\r\n return true;\r\n } catch (e) {\r\n return false;\r\n }\r\n }\r\n\r\n // ------------------------ sessionStorage 操作 ------------------------\r\n\r\n /**\r\n * 设置sessionStorage\r\n */\r\n private setSessionStorage(key: string, value: string): void {\r\n if (this.isSessionStorageAvailable()) {\r\n sessionStorage.setItem(key, value);\r\n }\r\n }\r\n\r\n /**\r\n * 获取sessionStorage\r\n */\r\n private getSessionStorage(key: string): string | null {\r\n if (this.isSessionStorageAvailable()) {\r\n return sessionStorage.getItem(key);\r\n }\r\n return null;\r\n }\r\n\r\n /**\r\n * 移除sessionStorage\r\n */\r\n private removeSessionStorage(key: string): void {\r\n if (this.isSessionStorageAvailable()) {\r\n sessionStorage.removeItem(key);\r\n }\r\n }\r\n\r\n /**\r\n * 清空sessionStorage中所有带前缀的项\r\n */\r\n private clearSessionStorage(): void {\r\n if (this.isSessionStorageAvailable()) {\r\n for (let i = 0; i < sessionStorage.length; i++) {\r\n const key = sessionStorage.key(i);\r\n if (key && key.startsWith(this.prefix)) {\r\n sessionStorage.removeItem(key);\r\n i--; // 索引调整\r\n }\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * 检查sessionStorage是否可用\r\n */\r\n private isSessionStorageAvailable(): boolean {\r\n if (typeof sessionStorage === 'undefined') {\r\n return false;\r\n }\r\n try {\r\n const testKey = '__storage_test__';\r\n sessionStorage.setItem(testKey, testKey);\r\n sessionStorage.removeItem(testKey);\r\n return true;\r\n } catch (e) {\r\n return false;\r\n }\r\n }\r\n\r\n // ------------------------ cookie 操作 ------------------------\r\n\r\n /**\r\n * 设置cookie\r\n */\r\n private setCookie(\r\n key: string,\r\n value: string,\r\n options?: { expires?: number; path?: string; domain?: string; secure?: boolean }\r\n ): void {\r\n if (typeof document === 'undefined') {\r\n return;\r\n }\r\n\r\n let cookieString = `${key}=${encodeURIComponent(value)}`;\r\n\r\n if (options) {\r\n // 设置过期时间(秒)\r\n if (options.expires) {\r\n const date = new Date();\r\n date.setTime(date.getTime() + options.expires * 1000);\r\n cookieString += `; expires=${date.toUTCString()}`;\r\n }\r\n\r\n // 设置路径\r\n if (options.path) {\r\n cookieString += `; path=${options.path}`;\r\n }\r\n\r\n // 设置域名\r\n if (options.domain) {\r\n cookieString += `; domain=${options.domain}`;\r\n }\r\n\r\n // 设置secure\r\n if (options.secure) {\r\n cookieString += '; secure';\r\n }\r\n }\r\n\r\n document.cookie = cookieString;\r\n }\r\n\r\n /**\r\n * 获取cookie\r\n */\r\n private getCookie(key: string): string | null {\r\n if (typeof document === 'undefined') {\r\n return null;\r\n }\r\n\r\n const name = `${key}=`;\r\n const decodedCookie = decodeURIComponent(document.cookie);\r\n const ca = decodedCookie.split(';');\r\n\r\n for (let i = 0; i < ca.length; i++) {\r\n let c = ca[i];\r\n while (c.charAt(0) === ' ') {\r\n c = c.substring(1);\r\n }\r\n if (c.indexOf(name) === 0) {\r\n return c.substring(name.length, c.length);\r\n }\r\n }\r\n\r\n return null;\r\n }\r\n\r\n /**\r\n * 移除cookie\r\n */\r\n private removeCookie(key: string): void {\r\n this.setCookie(key, '', { expires: -1 });\r\n }\r\n\r\n /**\r\n * 清空所有带前缀的cookie\r\n */\r\n private clearCookie(): void {\r\n if (typeof document === 'undefined') {\r\n return;\r\n }\r\n\r\n const cookies = document.cookie.split(';');\r\n for (const cookie of cookies) {\r\n const eqPos = cookie.indexOf('=');\r\n const key = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();\r\n if (key.startsWith(this.prefix)) {\r\n this.removeCookie(key);\r\n }\r\n }\r\n }\r\n}\r\n","/**\r\n * 路由守卫模块\r\n * 提供基于权限的路由拦截和未登录自动跳转登录页功能\r\n */\r\n\r\nimport { Auth } from '../core/auth';\r\n\r\n/**\r\n * 路由守卫选项\r\n */\r\nexport interface RouterGuardOptions {\r\n /**\r\n * 是否需要登录\r\n */\r\n requiresAuth?: boolean;\r\n /**\r\n * 需要的权限列表\r\n */\r\n requiredPermissions?: string[];\r\n /**\r\n * 登录后重定向的URL\r\n */\r\n redirectUri?: string;\r\n /**\r\n * 权限不足时重定向的URL\r\n */\r\n unauthorizedRedirectUri?: string;\r\n}\r\n\r\n/**\r\n * 路由守卫类\r\n */\r\nexport class RouterGuard {\r\n private auth: Auth;\r\n\r\n /**\r\n * 构造函数\r\n * @param auth 认证实例\r\n */\r\n constructor(auth: Auth) {\r\n this.auth = auth;\r\n }\r\n\r\n /**\r\n * 检查路由权限\r\n * @param options 路由守卫选项\r\n * @returns Promise 是否通过权限检查\r\n */\r\n async check(options: RouterGuardOptions): Promise {\r\n const { requiresAuth = true, requiredPermissions = [] } = options;\r\n\r\n // 检查是否需要登录\r\n if (requiresAuth) {\r\n // 检查是否已认证\r\n if (!this.auth.isAuthenticated()) {\r\n // 未认证,跳转到登录页\r\n this.auth.login(options.redirectUri);\r\n return false;\r\n }\r\n\r\n // 检查是否需要权限\r\n if (requiredPermissions.length > 0) {\r\n // 获取用户权限\r\n const userPermissions = [''];\r\n \r\n // 检查是否拥有所有需要的权限\r\n const hasPermission = requiredPermissions.every(permission => \r\n userPermissions.includes(permission)\r\n );\r\n\r\n if (!hasPermission) {\r\n // 权限不足,跳转到权限不足页\r\n if (options.unauthorizedRedirectUri) {\r\n window.location.href = options.unauthorizedRedirectUri;\r\n }\r\n return false;\r\n }\r\n }\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * 创建Vue路由守卫\r\n * @returns 路由守卫函数\r\n */\r\n createVueGuard() {\r\n return async (to: any, from: any, next: any) => {\r\n // 从路由元信息中获取守卫选项\r\n const options: RouterGuardOptions = to.meta?.auth || {};\r\n \r\n try {\r\n const allowed = await this.check(options);\r\n if (allowed) {\r\n next();\r\n }\r\n } catch (error) {\r\n console.error('Route guard error:', error);\r\n next(false);\r\n }\r\n };\r\n }\r\n\r\n /**\r\n * 检查当前用户是否有权限访问资源\r\n * @param permissions 需要的权限列表\r\n * @returns Promise 是否拥有权限\r\n */\r\n async hasPermission(permissions: string | string[]): Promise {\r\n if (!permissions) {\r\n return true;\r\n }\r\n\r\n const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions];\r\n \r\n // 检查是否已认证\r\n if (!this.auth.isAuthenticated()) {\r\n return false;\r\n }\r\n\r\n // 获取用户权限\r\n const userPermissions = ['']\r\n \r\n // 检查是否拥有所有需要的权限\r\n return requiredPermissions.every(permission => \r\n userPermissions.includes(permission)\r\n );\r\n }\r\n}\r\n","/**\r\n * Vue插件模块\r\n * 提供Vue应用中使用统一登录SDK的能力\r\n */\r\n\r\nimport { Auth } from '../core/auth';\r\nimport { SDKConfig } from '../types';\r\nimport { Storage } from '../utils/storage';\r\nimport { RouterGuard } from '../guards/router';\r\n\r\n/**\r\n * Vue插件选项\r\n */\r\nexport interface VuePluginOptions {\r\n /**\r\n * SDK配置\r\n */\r\n config: SDKConfig;\r\n /**\r\n * 插件名称,默认'unifiedLogin'\r\n */\r\n pluginName?: string;\r\n}\r\n\r\n/**\r\n * Vue插件类\r\n */\r\nexport class VuePlugin {\r\n private auth: Auth;\r\n private routerGuard: RouterGuard;\r\n\r\n /**\r\n * 构造函数\r\n * @param storage 存储实例\r\n */\r\n constructor(storage: Storage) {\r\n this.auth = new Auth(storage);\r\n this.routerGuard = new RouterGuard(this.auth);\r\n }\r\n\r\n /**\r\n * 安装Vue插件\r\n * @param app Vue构造函数或Vue 3应用实例\r\n * @param options 插件选项\r\n */\r\n install(app: any, options: VuePluginOptions): void {\r\n const { config, pluginName = 'unifiedLogin' } = options;\r\n\r\n // 初始化SDK\r\n this.auth.init(config);\r\n\r\n // 判断是Vue 2还是Vue 3\r\n const isVue3 = typeof app.config !== 'undefined';\r\n\r\n if (isVue3) {\r\n // Vue 3\r\n // 在全局属性上挂载SDK实例\r\n app.config.globalProperties[`${pluginName}`] = this.auth;\r\n app.config.globalProperties.$auth = this.auth; // 兼容简写\r\n\r\n // 提供Vue组件内的注入\r\n app.provide(pluginName, this.auth);\r\n app.provide('auth', this.auth); // 兼容简写\r\n\r\n // 处理路由守卫\r\n app.mixin({\r\n beforeCreate() {\r\n // 如果是根组件,添加路由守卫\r\n if (this.$options.router) {\r\n const router = this.$options.router;\r\n // 添加全局前置守卫\r\n router.beforeEach(this.routerGuard.createVueGuard());\r\n }\r\n }\r\n });\r\n } else {\r\n // Vue 2\r\n // 在Vue实例上挂载SDK实例\r\n app.prototype[`${pluginName}`] = this.auth;\r\n app.prototype.$auth = this.auth; // 兼容简写\r\n\r\n // 全局混入\r\n app.mixin({\r\n beforeCreate() {\r\n // 如果是根组件,添加路由守卫\r\n if (this.$options.router) {\r\n const router = this.$options.router;\r\n // 添加全局前置守卫\r\n router.beforeEach(this.routerGuard.createVueGuard());\r\n }\r\n }\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * 获取认证实例\r\n * @returns Auth 认证实例\r\n */\r\n getAuth(): Auth {\r\n return this.auth;\r\n }\r\n\r\n /**\r\n * 获取路由守卫实例\r\n * @returns RouterGuard 路由守卫实例\r\n */\r\n getRouterGuard(): RouterGuard {\r\n return this.routerGuard;\r\n }\r\n}\r\n\r\n/**\r\n * 创建Vue插件实例\r\n * @param storageType 存储类型\r\n * @returns VuePlugin Vue插件实例\r\n */\r\nexport function createVuePlugin(storageType?: 'localStorage' | 'sessionStorage' | 'cookie'): VuePlugin {\r\n const storage = new Storage(storageType);\r\n return new VuePlugin(storage);\r\n}\r\n","/**\r\n * 统一登录SDK入口文件\r\n * 支持OAuth2授权码模式,提供完整的Token管理和用户信息管理功能\r\n */\r\n\r\n// 导出核心类和功能\r\nexport { Auth } from './core/auth';\r\nexport { TokenManager } from './core/token';\r\nexport { HttpClient, HttpError } from './core/http';\r\nexport { Storage } from './utils/storage';\r\nexport { RouterGuard, RouterGuardOptions } from './guards/router';\r\n\r\n// 导出工具函数\r\nexport { \r\n generateRandomString, \r\n parseQueryParams, \r\n buildQueryParams, \r\n generateAuthorizationUrl, \r\n isCallbackUrl \r\n} from './utils/url';\r\n\r\n// 导出类型定义\r\nexport * from './types';\r\n\r\n// 导出Vue插件\r\nexport { VuePlugin, createVuePlugin } from './plugins/vue';\r\n\r\n// 创建默认SDK实例\r\nimport { SDKConfig, UnifiedLoginSDK } from './types';\r\nimport { Auth as AuthCore } from './core/auth';\r\nimport { Storage as StorageCore } from './utils/storage';\r\n\r\n/**\r\n * 默认SDK实例\r\n */\r\nconst defaultStorage = new StorageCore();\r\nconst defaultAuth = new AuthCore(defaultStorage);\r\n\r\n/**\r\n * 默认导出的SDK实例\r\n */\r\nexport const unifiedLoginSDK: UnifiedLoginSDK = {\r\n init: (config: SDKConfig) => {\r\n defaultAuth.init(config);\r\n },\r\n getToken: () => {\r\n return defaultAuth.getToken()\r\n },\r\n login: (redirectUri?: string) => {\r\n return defaultAuth.login(redirectUri);\r\n },\r\n logout: () => {\r\n return defaultAuth.logout();\r\n },\r\n handleCallback: () => {\r\n return defaultAuth.handleCallback();\r\n },\r\n getRoutes: () => {\r\n return defaultAuth.getRoutes();\r\n },\r\n getUserInfo: () => {\r\n return defaultAuth.getUserInfo();\r\n },\r\n isAuthenticated: () => {\r\n return defaultAuth.isAuthenticated();\r\n },\r\n hasRole: (role: string | string[]) => {\r\n return defaultAuth.hasRole(role);\r\n },\r\n hasAllRoles: (roles: string[]) => {\r\n return defaultAuth.hasAllRoles(roles);\r\n },\r\n hasPermission: (permission: string | string[]) => {\r\n return defaultAuth.hasPermission(permission);\r\n },\r\n hasAllPermissions: (permissions: string[]) => {\r\n return defaultAuth.hasAllPermissions(permissions);\r\n },\r\n on: (event, callback) => {\r\n return defaultAuth.on(event, callback);\r\n },\r\n off: (event, callback) => {\r\n return defaultAuth.off(event, callback);\r\n },\r\n isCallback: () => {\r\n return defaultAuth.isCallback();\r\n }\r\n};\r\n\r\n// 默认导出\r\nexport default unifiedLoginSDK;\r\n\r\n// 版本信息\r\nexport const version = '1.0.0';\r\n"],"names":["StorageCore","AuthCore"],"mappings":"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"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.d.ts b/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.d.ts new file mode 100644 index 0000000..f17e08a --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.d.ts @@ -0,0 +1,56 @@ +/** + * Vue插件模块 + * 提供Vue应用中使用统一登录SDK的能力 + */ +import { Auth } from '../core/auth'; +import { SDKConfig } from '../types'; +import { Storage } from '../utils/storage'; +import { RouterGuard } from '../guards/router'; +/** + * Vue插件选项 + */ +export interface VuePluginOptions { + /** + * SDK配置 + */ + config: SDKConfig; + /** + * 插件名称,默认'unifiedLogin' + */ + pluginName?: string; +} +/** + * Vue插件类 + */ +export declare class VuePlugin { + private auth; + private routerGuard; + /** + * 构造函数 + * @param storage 存储实例 + */ + constructor(storage: Storage); + /** + * 安装Vue插件 + * @param app Vue构造函数或Vue 3应用实例 + * @param options 插件选项 + */ + install(app: any, options: VuePluginOptions): void; + /** + * 获取认证实例 + * @returns Auth 认证实例 + */ + getAuth(): Auth; + /** + * 获取路由守卫实例 + * @returns RouterGuard 路由守卫实例 + */ + getRouterGuard(): RouterGuard; +} +/** + * 创建Vue插件实例 + * @param storageType 存储类型 + * @returns VuePlugin Vue插件实例 + */ +export declare function createVuePlugin(storageType?: 'localStorage' | 'sessionStorage' | 'cookie'): VuePlugin; +//# sourceMappingURL=vue.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.d.ts.map new file mode 100644 index 0000000..1e584fc --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"vue.d.ts","sourceRoot":"","sources":["../../src/plugins/vue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,MAAM,EAAE,SAAS,CAAC;IAClB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,WAAW,CAAc;IAEjC;;;OAGG;gBACS,OAAO,EAAE,OAAO;IAK5B;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAkDlD;;;OAGG;IACH,OAAO,IAAI,IAAI;IAIf;;;OAGG;IACH,cAAc,IAAI,WAAW;CAG9B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,WAAW,CAAC,EAAE,cAAc,GAAG,gBAAgB,GAAG,QAAQ,GAAG,SAAS,CAGrG"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.js b/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.js new file mode 100644 index 0000000..dac854d --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.js @@ -0,0 +1,93 @@ +/** + * Vue插件模块 + * 提供Vue应用中使用统一登录SDK的能力 + */ +import { Auth } from '../core/auth'; +import { Storage } from '../utils/storage'; +import { RouterGuard } from '../guards/router'; +/** + * Vue插件类 + */ +export class VuePlugin { + /** + * 构造函数 + * @param storage 存储实例 + */ + constructor(storage) { + this.auth = new Auth(storage); + this.routerGuard = new RouterGuard(this.auth); + } + /** + * 安装Vue插件 + * @param app Vue构造函数或Vue 3应用实例 + * @param options 插件选项 + */ + install(app, options) { + const { config, pluginName = 'unifiedLogin' } = options; + // 初始化SDK + this.auth.init(config); + // 判断是Vue 2还是Vue 3 + const isVue3 = typeof app.config !== 'undefined'; + if (isVue3) { + // Vue 3 + // 在全局属性上挂载SDK实例 + app.config.globalProperties[`${pluginName}`] = this.auth; + app.config.globalProperties.$auth = this.auth; // 兼容简写 + // 提供Vue组件内的注入 + app.provide(pluginName, this.auth); + app.provide('auth', this.auth); // 兼容简写 + // 处理路由守卫 + app.mixin({ + beforeCreate() { + // 如果是根组件,添加路由守卫 + if (this.$options.router) { + const router = this.$options.router; + // 添加全局前置守卫 + router.beforeEach(this.routerGuard.createVueGuard()); + } + } + }); + } + else { + // Vue 2 + // 在Vue实例上挂载SDK实例 + app.prototype[`${pluginName}`] = this.auth; + app.prototype.$auth = this.auth; // 兼容简写 + // 全局混入 + app.mixin({ + beforeCreate() { + // 如果是根组件,添加路由守卫 + if (this.$options.router) { + const router = this.$options.router; + // 添加全局前置守卫 + router.beforeEach(this.routerGuard.createVueGuard()); + } + } + }); + } + } + /** + * 获取认证实例 + * @returns Auth 认证实例 + */ + getAuth() { + return this.auth; + } + /** + * 获取路由守卫实例 + * @returns RouterGuard 路由守卫实例 + */ + getRouterGuard() { + return this.routerGuard; + } +} +/** + * 创建Vue插件实例 + * @param storageType 存储类型 + * @returns VuePlugin Vue插件实例 + */ +export function createVuePlugin(storageType) { + const storage = new Storage(storageType); + return new VuePlugin(storage); +} +//# sourceMappingURL=vue.js.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.js.map b/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.js.map new file mode 100644 index 0000000..c36a8c5 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/plugins/vue.js.map @@ -0,0 +1 @@ +{"version":3,"file":"vue.js","sourceRoot":"","sources":["../../src/plugins/vue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAgB/C;;GAEG;AACH,MAAM,OAAO,SAAS;IAIpB;;;OAGG;IACH,YAAY,OAAgB;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,WAAW,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,GAAQ,EAAE,OAAyB;QACzC,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC;QAExD,SAAS;QACT,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAEvB,kBAAkB;QAClB,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,WAAW,CAAC;QAEjD,IAAI,MAAM,EAAE,CAAC;YACX,QAAQ;YACR,gBAAgB;YAChB,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;YACzD,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO;YAEtD,cAAc;YACd,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YACnC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;YAEvC,SAAS;YACT,GAAG,CAAC,KAAK,CAAC;gBACR,YAAY;oBACV,gBAAgB;oBAChB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;wBACzB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;wBACpC,WAAW;wBACX,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC,CAAC;oBACvD,CAAC;gBACH,CAAC;aACF,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,QAAQ;YACR,iBAAiB;YACjB,GAAG,CAAC,SAAS,CAAC,GAAG,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;YAC3C,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO;YAExC,OAAO;YACP,GAAG,CAAC,KAAK,CAAC;gBACR,YAAY;oBACV,gBAAgB;oBAChB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;wBACzB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;wBACpC,WAAW;wBACX,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC,CAAC;oBACvD,CAAC;gBACH,CAAC;aACF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;OAGG;IACH,cAAc;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,WAA0D;IACxF,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC,CAAC;AAChC,CAAC"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/config.d.ts b/sdk/frontend/oauth2-login-sdk/dist/types/config.d.ts new file mode 100644 index 0000000..3fd4c65 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/config.d.ts @@ -0,0 +1,39 @@ +/** + * SDK配置选项 + */ +export interface SDKConfig { + /** 客户端ID */ + clientId: string; + /** 注册id **/ + registrationId: string; + /** 后端basepath路径*/ + basepath: string; + /** 存储类型,默认localStorage */ + storageType?: 'localStorage' | 'sessionStorage' | 'cookie'; + idpLogoutUrl: string; + homePage: string; + /** 租户ID(可选) */ + tenantId?: string; +} +/** + * Token信息 + */ +export interface TokenInfo { + /** 访问令牌 */ + accessToken: string; + /** 刷新令牌 */ + refreshToken: string; + /** 令牌类型,默认Bearer */ + tokenType?: string; + /** 访问令牌过期时间(秒) */ + expiresIn: number; + /** 刷新令牌过期时间(秒) */ + refreshExpiresIn?: number; + /** 令牌颁发时间戳 */ + issuedAt: number; +} +/** + * 事件类型 + */ +export type EventType = 'login' | 'logout' | 'tokenExpired'; +//# sourceMappingURL=config.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/config.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/types/config.d.ts.map new file mode 100644 index 0000000..4599a82 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/config.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,YAAY;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,cAAc,GAAG,gBAAgB,GAAG,QAAQ,CAAC;IAC3D,YAAY,EAAC,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,WAAW;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,cAAc,CAAC"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/config.js b/sdk/frontend/oauth2-login-sdk/dist/types/config.js new file mode 100644 index 0000000..79bd47b --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/config.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=config.js.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/config.js.map b/sdk/frontend/oauth2-login-sdk/dist/types/config.js.map new file mode 100644 index 0000000..307c78b --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/config.js.map @@ -0,0 +1 @@ +{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/index.d.ts b/sdk/frontend/oauth2-login-sdk/dist/types/index.d.ts new file mode 100644 index 0000000..e7396d2 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/index.d.ts @@ -0,0 +1,80 @@ +export * from './config'; +export * from './user'; +/** + * 统一登录SDK接口 + */ +export interface UnifiedLoginSDK { + /** + * 初始化SDK配置 + * @param config SDK配置选项 + */ + init(config: import('./config').SDKConfig): void; + getToken(): string | null; + /** + * 触发登录流程 + * @param redirectUri 可选的重定向URL,覆盖初始化时的配置 + */ + login(redirectUri?: string): Promise; + /** + * 退出登录 + */ + logout(): Promise; + /** + * 处理授权回调 + * @returns Promise 用户信息 + */ + handleCallback(): Promise; + getRoutes(): Promise; + /** + * 获取用户信息 + * @returns Promise 用户信息 + */ + getUserInfo(): import('./user').UserInfo; + /** + * 检查用户是否已认证 + * @returns boolean 是否已认证 + */ + isAuthenticated(): boolean; + /** + * 检查用户是否有指定角色 + * @param role 角色编码或角色编码列表 + * @returns Promise 是否有指定角色 + */ + hasRole(role: string | string[]): Promise; + /** + * 检查用户是否有所有指定角色 + * @param roles 角色编码列表 + * @returns Promise 是否有所有指定角色 + */ + hasAllRoles(roles: string[]): Promise; + /** + * 检查用户是否有指定权限 + * @param permission 权限标识或权限标识列表 + * @returns Promise 是否有指定权限 + */ + hasPermission(permission: string | string[]): Promise; + /** + * 检查用户是否有所有指定权限 + * @param permissions 权限标识列表 + * @returns Promise 是否有所有指定权限 + */ + hasAllPermissions(permissions: string[]): Promise; + /** + * 事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + on(event: import('./config').EventType, callback: Function): void; + /** + * 移除事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + off(event: import('./config').EventType, callback: Function): void; + /** + * 检查当前URL是否为授权回调 + * @returns boolean 是否为授权回调 + */ + isCallback(): boolean; +} +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/index.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/types/index.d.ts.map new file mode 100644 index 0000000..7b0f18b --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AAEvB;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,OAAO,UAAU,EAAE,SAAS,GAAG,IAAI,CAAC;IAEjD,QAAQ,IAAG,MAAM,GAAC,IAAI,CAAA;IACtB;;;OAGG;IACH,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3C;;OAEG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAExB;;;OAGG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,SAAS,IAAI,OAAO,CAAC,OAAO,QAAQ,EAAE,UAAU,CAAC,CAAC;IAElD;;;OAGG;IACH,WAAW,IAAI,OAAO,QAAQ,EAAE,QAAQ,CAAC;IAEzC;;;OAGG;IACH,eAAe,IAAI,OAAO,CAAC;IAE3B;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEnD;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE/C;;;;OAIG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE/D;;;;OAIG;IACH,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE3D;;;;OAIG;IACH,EAAE,CAAC,KAAK,EAAE,OAAO,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAElE;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,OAAO,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAEnE;;;OAGG;IACH,UAAU,IAAI,OAAO,CAAC;CACvB"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/index.js b/sdk/frontend/oauth2-login-sdk/dist/types/index.js new file mode 100644 index 0000000..7f54cb6 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/index.js @@ -0,0 +1,3 @@ +export * from './config'; +export * from './user'; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/index.js.map b/sdk/frontend/oauth2-login-sdk/dist/types/index.js.map new file mode 100644 index 0000000..892d9fc --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/user.d.ts b/sdk/frontend/oauth2-login-sdk/dist/types/user.d.ts new file mode 100644 index 0000000..f350b89 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/user.d.ts @@ -0,0 +1,83 @@ +/** + * 菜单信息 + */ +export interface RouterInfo { + /** 菜单名称 */ + name: string; + /** 菜单路径 */ + path?: string; + hidden: boolean; + redirect: string; + query: string; + alwaysShow: boolean; + /** 菜单组件 */ + component?: string; + meta: MetaVo; + children: RouterInfo; +} +export interface MetaVo { + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + title: string; + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + icon: string; + /** + * 设置为true,则不会被 缓存 + */ + noCache: boolean; + /** + * 内链地址(http(s)://开头) + */ + link: string; +} +/** + * 用户基本信息 + */ +export interface UserInfo { + /** 用户ID */ + userId: string; + /** 用户名 */ + username: string; + /** 姓名 */ + nickName: string; + /** 邮箱 */ + currentDeptId: string; + /** 部门 */ + userDepts?: UserDept[]; + /** 岗位 */ + userPost?: UserPost[]; + /** 性别 */ + sex: string; + /** 用户角色 */ + roles?: string[]; + /** 权限列表 */ + permissions?: string[]; + dataPermission: DataPermission; +} +export interface DataPermission { + allowAll: boolean; + onlySelf: boolean; + deptList?: string[]; + areas?: string[]; +} +export interface UserDept { + postCode: string; + postId: bigint; + postName: string; + postSort: bigint; + remark: string; + status: bigint; +} +export interface UserPost { + ancestors: string; + deptId: bigint; + deptName: string; + leader: string; + orderNum: bigint; + parentId: bigint; + status: bigint; +} +//# sourceMappingURL=user.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/user.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/types/user.d.ts.map new file mode 100644 index 0000000..c19c88c --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/user.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/types/user.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,WAAW;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAC,MAAM,CAAC;IACZ,QAAQ,EAAE,UAAU,CAAC;CACtB;AACD,MAAM,WAAW,MAAM;IACrB;;OAEG;IACH,KAAK,EAAC,MAAM,CAAC;IAEb;;OAEG;IACH,IAAI,EAAC,MAAM,CAAC;IAEZ;;OAEG;IACH,OAAO,EAAC,OAAO,CAAC;IAEhB;;OAEG;IACH,IAAI,EAAC,MAAM,CAAC;CACb;AAGD;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,WAAW;IACX,MAAM,EAAE,MAAM,CAAC;IACf,UAAU;IACV,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS;IACT,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS;IACT,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,SAAS;IACT,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;IACtB,SAAS;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW;IACX,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW;IACX,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,cAAc,CAAA;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAC,MAAM,EAAE,CAAA;CAChB;AACD,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAC,MAAM,CAAA;IACf,MAAM,EAAC,MAAM,CAAA;IACb,QAAQ,EAAC,MAAM,CAAA;IACf,QAAQ,EAAC,MAAM,CAAA;IACf,MAAM,EAAC,MAAM,CAAA;IACb,MAAM,EAAC,MAAM,CAAA;CACd;AACD,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/user.js b/sdk/frontend/oauth2-login-sdk/dist/types/user.js new file mode 100644 index 0000000..8f7afc5 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/user.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=user.js.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/types/user.js.map b/sdk/frontend/oauth2-login-sdk/dist/types/user.js.map new file mode 100644 index 0000000..07c1726 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/types/user.js.map @@ -0,0 +1 @@ +{"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/types/user.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/utils/storage.d.ts b/sdk/frontend/oauth2-login-sdk/dist/utils/storage.d.ts new file mode 100644 index 0000000..ec62a69 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/utils/storage.d.ts @@ -0,0 +1,108 @@ +/** + * 存储工具类 + * 支持localStorage、sessionStorage和cookie三种存储方式 + */ +type StorageType = 'localStorage' | 'sessionStorage' | 'cookie'; +/** + * 存储工具类 + */ +export declare class Storage { + private storageType; + private prefix; + /** + * 构造函数 + * @param storageType 存储类型 + * @param prefix 存储前缀,默认'unified_login_' + */ + constructor(storageType?: StorageType, prefix?: string); + /** + * 设置存储项 + * @param key 存储键 + * @param value 存储值 + * @param options 可选参数,cookie存储时使用 + */ + set(key: string, value: any, options?: { + expires?: number; + path?: string; + domain?: string; + secure?: boolean; + }): void; + /** + * 获取存储项 + * @param key 存储键 + * @returns 存储值 + */ + get(key: string): any; + /** + * 移除存储项 + * @param key 存储键 + */ + remove(key: string): void; + /** + * 清空所有存储项 + */ + clear(): void; + /** + * 检查存储类型是否可用 + * @returns boolean 是否可用 + */ + isAvailable(): boolean; + /** + * 设置localStorage + */ + private setLocalStorage; + /** + * 获取localStorage + */ + private getLocalStorage; + /** + * 移除localStorage + */ + private removeLocalStorage; + /** + * 清空localStorage中所有带前缀的项 + */ + private clearLocalStorage; + /** + * 检查localStorage是否可用 + */ + private isLocalStorageAvailable; + /** + * 设置sessionStorage + */ + private setSessionStorage; + /** + * 获取sessionStorage + */ + private getSessionStorage; + /** + * 移除sessionStorage + */ + private removeSessionStorage; + /** + * 清空sessionStorage中所有带前缀的项 + */ + private clearSessionStorage; + /** + * 检查sessionStorage是否可用 + */ + private isSessionStorageAvailable; + /** + * 设置cookie + */ + private setCookie; + /** + * 获取cookie + */ + private getCookie; + /** + * 移除cookie + */ + private removeCookie; + /** + * 清空所有带前缀的cookie + */ + private clearCookie; +} +export {}; +//# sourceMappingURL=storage.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/utils/storage.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/utils/storage.d.ts.map new file mode 100644 index 0000000..3768e0f --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/utils/storage.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/utils/storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,KAAK,WAAW,GAAG,cAAc,GAAG,gBAAgB,GAAG,QAAQ,CAAC;AAEhE;;GAEG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,MAAM,CAAS;IAEvB;;;;OAIG;gBACS,WAAW,GAAE,WAA4B,EAAE,MAAM,GAAE,MAAyB;IAKxF;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IAiBpH;;;;OAIG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG;IA+BrB;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAgBzB;;OAEG;IACH,KAAK,IAAI,IAAI;IAcb;;;OAGG;IACH,WAAW,IAAI,OAAO;IAmBtB;;OAEG;IACH,OAAO,CAAC,eAAe;IAMvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAYzB;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAgB/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAMzB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAOzB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAM5B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAY3B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAgBjC;;OAEG;IACH,OAAO,CAAC,SAAS;IAsCjB;;OAEG;IACH,OAAO,CAAC,SAAS;IAsBjB;;OAEG;IACH,OAAO,CAAC,YAAY;IAIpB;;OAEG;IACH,OAAO,CAAC,WAAW;CAcpB"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/utils/storage.js b/sdk/frontend/oauth2-login-sdk/dist/utils/storage.js new file mode 100644 index 0000000..039ba97 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/utils/storage.js @@ -0,0 +1,316 @@ +/** + * 存储工具类 + * 支持localStorage、sessionStorage和cookie三种存储方式 + */ +/** + * 存储工具类 + */ +export class Storage { + /** + * 构造函数 + * @param storageType 存储类型 + * @param prefix 存储前缀,默认'unified_login_' + */ + constructor(storageType = 'localStorage', prefix = 'unified_login_') { + this.storageType = storageType; + this.prefix = prefix; + } + /** + * 设置存储项 + * @param key 存储键 + * @param value 存储值 + * @param options 可选参数,cookie存储时使用 + */ + set(key, value, options) { + const fullKey = this.prefix + key; + const stringValue = typeof value === 'string' ? value : JSON.stringify(value); + switch (this.storageType) { + case 'localStorage': + this.setLocalStorage(fullKey, stringValue); + break; + case 'sessionStorage': + this.setSessionStorage(fullKey, stringValue); + break; + case 'cookie': + this.setCookie(fullKey, stringValue, options); + break; + } + } + /** + * 获取存储项 + * @param key 存储键 + * @returns 存储值 + */ + get(key) { + const fullKey = this.prefix + key; + let value; + switch (this.storageType) { + case 'localStorage': + value = this.getLocalStorage(fullKey); + break; + case 'sessionStorage': + value = this.getSessionStorage(fullKey); + break; + case 'cookie': + value = this.getCookie(fullKey); + break; + default: + value = null; + } + if (value === null) { + return null; + } + // 尝试解析JSON + try { + return JSON.parse(value); + } + catch (e) { + // 如果不是JSON,直接返回字符串 + return value; + } + } + /** + * 移除存储项 + * @param key 存储键 + */ + remove(key) { + const fullKey = this.prefix + key; + switch (this.storageType) { + case 'localStorage': + this.removeLocalStorage(fullKey); + break; + case 'sessionStorage': + this.removeSessionStorage(fullKey); + break; + case 'cookie': + this.removeCookie(fullKey); + break; + } + } + /** + * 清空所有存储项 + */ + clear() { + switch (this.storageType) { + case 'localStorage': + this.clearLocalStorage(); + break; + case 'sessionStorage': + this.clearSessionStorage(); + break; + case 'cookie': + this.clearCookie(); + break; + } + } + /** + * 检查存储类型是否可用 + * @returns boolean 是否可用 + */ + isAvailable() { + try { + switch (this.storageType) { + case 'localStorage': + return this.isLocalStorageAvailable(); + case 'sessionStorage': + return this.isSessionStorageAvailable(); + case 'cookie': + return typeof document !== 'undefined'; + default: + return false; + } + } + catch (e) { + return false; + } + } + // ------------------------ localStorage 操作 ------------------------ + /** + * 设置localStorage + */ + setLocalStorage(key, value) { + if (this.isLocalStorageAvailable()) { + localStorage.setItem(key, value); + } + } + /** + * 获取localStorage + */ + getLocalStorage(key) { + if (this.isLocalStorageAvailable()) { + return localStorage.getItem(key); + } + return null; + } + /** + * 移除localStorage + */ + removeLocalStorage(key) { + if (this.isLocalStorageAvailable()) { + localStorage.removeItem(key); + } + } + /** + * 清空localStorage中所有带前缀的项 + */ + clearLocalStorage() { + if (this.isLocalStorageAvailable()) { + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(this.prefix)) { + localStorage.removeItem(key); + i--; // 索引调整 + } + } + } + } + /** + * 检查localStorage是否可用 + */ + isLocalStorageAvailable() { + if (typeof localStorage === 'undefined') { + return false; + } + try { + const testKey = '__storage_test__'; + localStorage.setItem(testKey, testKey); + localStorage.removeItem(testKey); + return true; + } + catch (e) { + return false; + } + } + // ------------------------ sessionStorage 操作 ------------------------ + /** + * 设置sessionStorage + */ + setSessionStorage(key, value) { + if (this.isSessionStorageAvailable()) { + sessionStorage.setItem(key, value); + } + } + /** + * 获取sessionStorage + */ + getSessionStorage(key) { + if (this.isSessionStorageAvailable()) { + return sessionStorage.getItem(key); + } + return null; + } + /** + * 移除sessionStorage + */ + removeSessionStorage(key) { + if (this.isSessionStorageAvailable()) { + sessionStorage.removeItem(key); + } + } + /** + * 清空sessionStorage中所有带前缀的项 + */ + clearSessionStorage() { + if (this.isSessionStorageAvailable()) { + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + if (key && key.startsWith(this.prefix)) { + sessionStorage.removeItem(key); + i--; // 索引调整 + } + } + } + } + /** + * 检查sessionStorage是否可用 + */ + isSessionStorageAvailable() { + if (typeof sessionStorage === 'undefined') { + return false; + } + try { + const testKey = '__storage_test__'; + sessionStorage.setItem(testKey, testKey); + sessionStorage.removeItem(testKey); + return true; + } + catch (e) { + return false; + } + } + // ------------------------ cookie 操作 ------------------------ + /** + * 设置cookie + */ + setCookie(key, value, options) { + if (typeof document === 'undefined') { + return; + } + let cookieString = `${key}=${encodeURIComponent(value)}`; + if (options) { + // 设置过期时间(秒) + if (options.expires) { + const date = new Date(); + date.setTime(date.getTime() + options.expires * 1000); + cookieString += `; expires=${date.toUTCString()}`; + } + // 设置路径 + if (options.path) { + cookieString += `; path=${options.path}`; + } + // 设置域名 + if (options.domain) { + cookieString += `; domain=${options.domain}`; + } + // 设置secure + if (options.secure) { + cookieString += '; secure'; + } + } + document.cookie = cookieString; + } + /** + * 获取cookie + */ + getCookie(key) { + if (typeof document === 'undefined') { + return null; + } + const name = `${key}=`; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return null; + } + /** + * 移除cookie + */ + removeCookie(key) { + this.setCookie(key, '', { expires: -1 }); + } + /** + * 清空所有带前缀的cookie + */ + clearCookie() { + if (typeof document === 'undefined') { + return; + } + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const eqPos = cookie.indexOf('='); + const key = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + if (key.startsWith(this.prefix)) { + this.removeCookie(key); + } + } + } +} +//# sourceMappingURL=storage.js.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/utils/storage.js.map b/sdk/frontend/oauth2-login-sdk/dist/utils/storage.js.map new file mode 100644 index 0000000..ac897c6 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/utils/storage.js.map @@ -0,0 +1 @@ +{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/utils/storage.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/utils/url.d.ts b/sdk/frontend/oauth2-login-sdk/dist/utils/url.d.ts new file mode 100644 index 0000000..8d92191 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/utils/url.d.ts @@ -0,0 +1,55 @@ +/** + * URL处理工具 + * 用于生成授权URL、解析URL参数等功能 + */ +/** + * 生成随机字符串 + * @param length 字符串长度,默认32位 + * @returns 随机字符串 + */ +export declare function generateRandomString(length?: number): string; +/** + * 解析URL查询参数 + * @param url URL字符串,默认为当前URL + * @returns 查询参数对象 + */ +export declare function parseQueryParams(url?: string): Record; +/** + * 构建URL查询参数 + * @param params 查询参数对象 + * @returns 查询参数字符串 + */ +export declare function buildQueryParams(params: Record): string; +/** + * 生成OAuth2授权URL + * @param authorizationEndpoint 授权端点URL + * @param clientId 客户端ID + * @param redirectUri 重定向URL + * @param options 可选参数 + * @returns 授权URL + */ +export declare function generateAuthorizationUrl(authorizationEndpoint: string, clientId: string, redirectUri: string, options?: { + responseType?: string; + scope?: string; + state?: string; + [key: string]: any; +}): string; +/** + * 检查当前URL是否为授权回调 + * @param url URL字符串,默认为当前URL + * @returns 是否为授权回调 + */ +export declare function isCallbackUrl(url?: string): boolean; +/** + * 获取当前URL的路径名 + * @param url URL字符串,默认为当前URL + * @returns 路径名 + */ +export declare function getPathname(url?: string): string; +/** + * 获取当前URL的主机名 + * @param url URL字符串,默认为当前URL + * @returns 主机名 + */ +export declare function getHostname(url?: string): string; +//# sourceMappingURL=url.d.ts.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/utils/url.d.ts.map b/sdk/frontend/oauth2-login-sdk/dist/utils/url.d.ts.map new file mode 100644 index 0000000..085e247 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/utils/url.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/utils/url.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAOhE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAA6B,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgB3F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAQpE;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,qBAAqB,EAAE,MAAM,EAC7B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IACR,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,GACA,MAAM,CAmBR;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAA6B,GAAG,OAAO,CAGzE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,GAAE,MAA6B,GAAG,MAAM,CAGtE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,GAAE,MAA6B,GAAG,MAAM,CAGtE"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/utils/url.js b/sdk/frontend/oauth2-login-sdk/dist/utils/url.js new file mode 100644 index 0000000..fc8750a --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/utils/url.js @@ -0,0 +1,100 @@ +/** + * URL处理工具 + * 用于生成授权URL、解析URL参数等功能 + */ +/** + * 生成随机字符串 + * @param length 字符串长度,默认32位 + * @returns 随机字符串 + */ +export function generateRandomString(length = 32) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} +/** + * 解析URL查询参数 + * @param url URL字符串,默认为当前URL + * @returns 查询参数对象 + */ +export function parseQueryParams(url = window.location.href) { + const params = {}; + const queryString = url.split('?')[1]; + if (!queryString) { + return params; + } + const pairs = queryString.split('&'); + for (const pair of pairs) { + const [key, value] = pair.split('='); + if (key) { + params[decodeURIComponent(key)] = decodeURIComponent(value || ''); + } + } + return params; +} +/** + * 构建URL查询参数 + * @param params 查询参数对象 + * @returns 查询参数字符串 + */ +export function buildQueryParams(params) { + const pairs = []; + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); + } + } + return pairs.length ? `?${pairs.join('&')}` : ''; +} +/** + * 生成OAuth2授权URL + * @param authorizationEndpoint 授权端点URL + * @param clientId 客户端ID + * @param redirectUri 重定向URL + * @param options 可选参数 + * @returns 授权URL + */ +export function generateAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, options) { + const { responseType = 'code', scope, state = generateRandomString(32), ...extraParams } = options || {}; + const params = { + client_id: clientId, + redirect_uri: redirectUri, + response_type: responseType, + state, + ...(scope ? { scope } : {}), + ...extraParams + }; + const queryString = buildQueryParams(params); + return `${authorizationEndpoint}${queryString}`; +} +/** + * 检查当前URL是否为授权回调 + * @param url URL字符串,默认为当前URL + * @returns 是否为授权回调 + */ +export function isCallbackUrl(url = window.location.href) { + const params = parseQueryParams(url); + return !!params.code || !!params.error; +} +/** + * 获取当前URL的路径名 + * @param url URL字符串,默认为当前URL + * @returns 路径名 + */ +export function getPathname(url = window.location.href) { + const urlObj = new URL(url); + return urlObj.pathname; +} +/** + * 获取当前URL的主机名 + * @param url URL字符串,默认为当前URL + * @returns 主机名 + */ +export function getHostname(url = window.location.href) { + const urlObj = new URL(url); + return urlObj.hostname; +} +//# sourceMappingURL=url.js.map \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/dist/utils/url.js.map b/sdk/frontend/oauth2-login-sdk/dist/utils/url.js.map new file mode 100644 index 0000000..9cb04c2 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/dist/utils/url.js.map @@ -0,0 +1 @@ +{"version":3,"file":"url.js","sourceRoot":"","sources":["../../src/utils/url.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/sdk/frontend/oauth2-login-sdk/package.json b/sdk/frontend/oauth2-login-sdk/package.json new file mode 100644 index 0000000..5bc200b --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/package.json @@ -0,0 +1,41 @@ +{ + "name": "oauth2-login-sdk", + "version": "1.0.0", + "description": "TypeScript前端SDK,用于前后端分离项目对接统一登录系统", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc && rollup -c", + "dev": "tsc -w", + "test": "jest", + "lint": "eslint src --ext .ts", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "oauth2", + "login", + "sdk", + "typescript", + "unified-login" + ], + "author": "", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.11.16", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.56.0", + "jest-environment-jsdom": "^30.2.0", + "rollup": "^4.9.6", + "rollup-plugin-typescript2": "^0.36.0", + "typescript": "^5.3.3" + }, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=16.0.0" + } +} diff --git a/sdk/frontend/oauth2-login-sdk/pnpm-lock.yaml b/sdk/frontend/oauth2-login-sdk/pnpm-lock.yaml new file mode 100644 index 0000000..e292df9 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/pnpm-lock.yaml @@ -0,0 +1,2103 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^20.11.16 + version: 20.19.30 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.56.0 + version: 8.57.1 + jest-environment-jsdom: + specifier: ^30.2.0 + version: 30.2.0 + rollup: + specifier: ^4.9.6 + version: 4.57.1 + rollup-plugin-typescript2: + specifier: ^0.36.0 + version: 0.36.0(rollup@4.57.1)(typescript@5.9.3) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + +packages: + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@jest/environment-jsdom-abstract@30.2.0': + resolution: {integrity: sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + jsdom: '*' + peerDependenciesMeta: + canvas: + optional: true + + '@jest/environment@30.2.0': + resolution: {integrity: sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@30.2.0': + resolution: {integrity: sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/types@30.2.0': + resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + + '@sinclair/typebox@0.34.48': + resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@13.0.5': + resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jsdom@21.1.7': + resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@6.21.0': + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@6.21.0': + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@6.21.0': + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/type-utils@6.21.0': + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@6.21.0': + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/typescript-estree@6.21.0': + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@6.21.0': + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@6.21.0': + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jest-environment-jsdom@30.2.0: + resolution: {integrity: sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jest-message-util@30.2.0: + resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-mock@30.2.0: + resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-util@30.2.0: + resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@30.2.0: + resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup-plugin-typescript2@0.36.0: + resolution: {integrity: sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw==} + peerDependencies: + rollup: '>=1.26.3' + typescript: '>=2.4.0' + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.28.5': {} + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@jest/environment-jsdom-abstract@30.2.0(jsdom@26.1.0)': + dependencies: + '@jest/environment': 30.2.0 + '@jest/fake-timers': 30.2.0 + '@jest/types': 30.2.0 + '@types/jsdom': 21.1.7 + '@types/node': 20.19.30 + jest-mock: 30.2.0 + jest-util: 30.2.0 + jsdom: 26.1.0 + + '@jest/environment@30.2.0': + dependencies: + '@jest/fake-timers': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 20.19.30 + jest-mock: 30.2.0 + + '@jest/fake-timers@30.2.0': + dependencies: + '@jest/types': 30.2.0 + '@sinonjs/fake-timers': 13.0.5 + '@types/node': 20.19.30 + jest-message-util: 30.2.0 + jest-mock: 30.2.0 + jest-util: 30.2.0 + + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 20.19.30 + jest-regex-util: 30.0.1 + + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.48 + + '@jest/types@30.2.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.19.30 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@sinclair/typebox@0.34.48': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@13.0.5': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@types/estree@1.0.8': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jsdom@21.1.7': + dependencies: + '@types/node': 20.19.30 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + + '@types/json-schema@7.0.15': {} + + '@types/node@20.19.30': + dependencies: + undici-types: 6.21.0 + + '@types/semver@7.7.1': {} + + '@types/stack-utils@2.0.3': {} + + '@types/tough-cookie@4.0.5': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + semver: 7.7.3 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + eslint: 8.57.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@6.21.0': {} + + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.7.3 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + eslint: 8.57.1 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.3.0': {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@7.1.4: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + ci-info@4.4.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commondir@1.0.1: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + entities@6.0.1: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-potential-custom-element-name@1.0.1: {} + + isexe@2.0.0: {} + + jest-environment-jsdom@30.2.0: + dependencies: + '@jest/environment': 30.2.0 + '@jest/environment-jsdom-abstract': 30.2.0(jsdom@26.1.0) + '@types/jsdom': 21.1.7 + '@types/node': 20.19.30 + jsdom: 26.1.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-message-util@30.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 30.2.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@30.2.0: + dependencies: + '@jest/types': 30.2.0 + '@types/node': 20.19.30 + jest-util: 30.2.0 + + jest-regex-util@30.0.1: {} + + jest-util@30.2.0: + dependencies: + '@jest/types': 30.2.0 + '@types/node': 20.19.30 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lru-cache@10.4.3: {} + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.2 + + ms@2.1.3: {} + + natural-compare@1.4.0: {} + + nwsapi@2.2.23: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + prelude-ls@1.2.1: {} + + pretty-format@30.2.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-is@18.3.1: {} + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup-plugin-typescript2@0.36.0(rollup@4.57.1)(typescript@5.9.3): + dependencies: + '@rollup/pluginutils': 4.2.1 + find-cache-dir: 3.3.2 + fs-extra: 10.1.0 + rollup: 4.57.1 + semver: 7.7.3 + tslib: 2.8.1 + typescript: 5.9.3 + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + semver@6.3.1: {} + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + slash@3.0.0: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + + text-table@0.2.0: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + ts-api-utils@1.4.3(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.20.2: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + universalify@2.0.1: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yocto-queue@0.1.0: {} diff --git a/sdk/frontend/oauth2-login-sdk/rollup.config.js b/sdk/frontend/oauth2-login-sdk/rollup.config.js new file mode 100644 index 0000000..3ac5c7c --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/rollup.config.js @@ -0,0 +1,23 @@ +import typescript from 'rollup-plugin-typescript2'; + +export default { + input: 'src/index.ts', + output: [ + { + file: 'dist/index.js', + format: 'cjs', + sourcemap: true + }, + { + file: 'dist/index.esm.js', + format: 'esm', + sourcemap: true + } + ], + plugins: [ + typescript({ + tsconfig: './tsconfig.json', + clean: true + }) + ] +}; diff --git a/sdk/frontend/oauth2-login-sdk/src/core/auth.ts b/sdk/frontend/oauth2-login-sdk/src/core/auth.ts new file mode 100644 index 0000000..9a9ea2b --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/core/auth.ts @@ -0,0 +1,280 @@ +/** + * 认证核心逻辑 + * 实现OAuth2授权码模式的完整流程 + */ + +import {EventType, RouterInfo, SDKConfig, UserInfo} from '../types'; +import {TokenManager} from './token'; +import {HttpClient} from './http'; +import {Storage} from '../utils/storage'; +import {buildQueryParams, isCallbackUrl, parseQueryParams} from '../utils/url'; + +/** + * 认证核心类 + */ +export class Auth { + private config: SDKConfig | null = null; + private tokenManager: TokenManager; + private httpClient: HttpClient; + private storage: Storage; + private eventHandlers: Record = { + login: [], + logout: [], + tokenExpired: [] + }; + private userInfoCache: UserInfo | null = null; + + /** + * 构造函数 + * @param storage 存储实例 + */ + constructor(storage: Storage) { + this.storage = storage; + // 先创建HttpClient,初始时tokenManager为undefined + this.httpClient = new HttpClient(() => this.tokenManager.getToken() || null); + // 然后创建TokenManager + this.tokenManager = new TokenManager(storage); + } + + /** + * 初始化SDK配置 + * @param config SDK配置选项 + */ + init(config: SDKConfig): void { + this.config = config; + // 设置租户ID到HTTP客户端 + this.httpClient.setTenantId(config.tenantId); + } + + getToken():string | null{ + return this.tokenManager.getToken() + } + + /** + * 触发登录流程 + * @param redirectUri 可选的重定向URL,覆盖初始化时的配置 + */ + async login(redirectUri?: string): Promise { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const registrationId = this.config.registrationId || 'idp' + const basepath = this.config.basepath || '' + const path = `${basepath}/oauth2/authorization/${registrationId}` + const tokenResponse = await this.httpClient.get(path,{needAuth:false}) + const redirect = tokenResponse.data.redirect_url + const params = parseQueryParams(redirect) + this.storage.set(params.state,window.location.href) + window.location.href = redirect + } + + /** + * 退出登录 + */ + async logout(): Promise { + if (!this.config) { + throw new Error('SDK not initialized'); + } + // 清除本地存储的Token和用户信息 + this.tokenManager.clearToken(); + this.userInfoCache = null; + this.storage.remove('userInfo'); + const basepath = this.config.basepath || '' + await this.httpClient.post(`${basepath}/logout`,null,{needAuth:true}) + // 触发退出事件 + this.emit('logout'); + window.location.href = this.config.idpLogoutUrl+'?redirect='+this.config.homePage; + } + + /** + * 处理授权回调 + * @returns Promise 用户信息 + */ + async handleCallback(): Promise { + if (!this.config) { + throw new Error('SDK not initialized'); + } + + const params = parseQueryParams(); + + // 检查是否有错误 + if (params.error) { + throw new Error(`Authorization error: ${params.error} - ${params.error_description || ''}`); + } + + // 检查是否有授权码 + if (!params.code) { + throw new Error('Authorization code not found'); + } + + const registrationId = this.config.registrationId || 'idp' + const basepath = this.config.basepath || '' + const callback = `${basepath}/login/oauth2/code/${registrationId}${buildQueryParams(params)}` + const tokenResponse = await this.httpClient.get(callback,{ + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + needAuth: false + }) + // 触发登录事件 + this.emit('login'); + this.storage.set('userInfo', tokenResponse.data.data); + this.tokenManager.saveToken(tokenResponse.headers['authorization']||tokenResponse.headers['Authorization']) + let url = this.config.homePage + if(params.state){ + url = this.storage.get(params.state) || url; + } + window.location.href = url; + + } + + async getRoutes(): Promise { + if (!this.config) { + throw new Error('SDK not initialized'); + } + const basepath = this.config.basepath || '' + const tokenResponse = await this.httpClient.get(`${basepath}/idp/routes`,{needAuth:true}) + if(tokenResponse.status===401){ + await this.logout() + } + return tokenResponse.data.data + + } + /** + * 获取用户信息 + * @returns UserInfo 用户信息 + */ + getUserInfo(): UserInfo { + return this.storage.get("userInfo"); + } + + + + + /** + * 检查用户是否有指定角色 + * @param role 角色编码或角色编码列表 + * @returns Promise 是否有指定角色 + */ + async hasRole(role: string | string[]): Promise { + if (!this.isAuthenticated()) { + return false; + } + + const userInfo:UserInfo = this.storage.get("userInfo"); + const roleCodes = userInfo.roles||[]; + + if (Array.isArray(role)) { + // 检查是否有任一角色 + return role.some(r => roleCodes.includes(r)); + } + + // 检查是否有单个角色 + return roleCodes.includes(role); + } + + /** + * 检查用户是否有所有指定角色 + * @param roles 角色编码列表 + * @returns Promise 是否有所有指定角色 + */ + async hasAllRoles(roles: string[]): Promise { + if (!this.isAuthenticated()) { + return false; + } + + const userInfo:UserInfo = this.storage.get("userInfo"); + const roleCodes = userInfo.roles||[]; + // 检查是否有所有角色 + return roles.every(r => roleCodes.includes(r)); + } + + /** + * 检查用户是否有指定权限 + * @param permission 权限标识或权限标识列表 + * @returns Promise 是否有指定权限 + */ + async hasPermission(permission: string | string[]): Promise { + if (!this.isAuthenticated()) { + return false; + } + + const userInfo:UserInfo = this.storage.get("userInfo"); + const permissions = userInfo.permissions||[]; + + if (Array.isArray(permission)) { + // 检查是否有任一权限 + return permission.some(p => permissions.includes(p)); + } + + // 检查是否有单个权限 + return permissions.includes(permission); + } + + /** + * 检查用户是否有所有指定权限 + * @param permissions 权限标识列表 + * @returns Promise 是否有所有指定权限 + */ + async hasAllPermissions(permissions: string[]): Promise { + if (!this.isAuthenticated()) { + return false; + } + + const userInfo:UserInfo = this.storage.get("userInfo"); + const userPermissions = userInfo.permissions||[]; + + // 检查是否有所有权限 + return permissions.every(p => userPermissions.includes(p)); + } + + /** + * 检查用户是否已认证 + * @returns boolean 是否已认证 + */ + isAuthenticated(): boolean { + // 检查Token是否存在且未过期 + return !!this.tokenManager.getToken(); + } + + /** + * 事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + on(event: EventType, callback: Function): void { + this.eventHandlers[event].push(callback); + } + + /** + * 移除事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + off(event: EventType, callback: Function): void { + this.eventHandlers[event] = this.eventHandlers[event].filter(handler => handler !== callback); + } + + /** + * 触发事件 + * @param event 事件类型 + * @param data 事件数据 + */ + private emit(event: EventType, data?: any): void { + this.eventHandlers[event].forEach(handler => { + try { + handler(data); + } catch (error) { + console.error(`Error in ${event} event handler:`, error); + } + }); + } + + /** + * 检查当前URL是否为授权回调 + * @returns boolean 是否为授权回调 + */ + isCallback(): boolean { + return isCallbackUrl(); + } +} diff --git a/sdk/frontend/oauth2-login-sdk/src/core/http.ts b/sdk/frontend/oauth2-login-sdk/src/core/http.ts new file mode 100644 index 0000000..6fc14be --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/core/http.ts @@ -0,0 +1,370 @@ +/** + * HTTP客户端 + * 用于与后端API进行通信 + */ + +/** + * HTTP请求方法类型 + */ +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + +/** + * HTTP请求选项 + */ +export interface HttpRequestOptions { + /** 请求方法 */ + method: HttpMethod; + /** 请求URL */ + url: string; + /** 请求头 */ + headers?: Record; + /** 请求体 */ + body?: any; + /** 是否需要认证 */ + needAuth?: boolean; +} + +/** + * HTTP响应类型 + */ +export interface HttpResponse { + /** 状态码 */ + status: number; + /** 状态文本 */ + statusText: string; + /** 响应体 */ + data: T; + /** 响应头 */ + headers: Record; +} + +/** + * HTTP错误类型 + */ +export class HttpError extends Error { + /** 状态码 */ + public status: number; + /** 状态文本 */ + public statusText: string; + /** 错误数据 */ + public data: any; + + /** + * 构造函数 + * @param message 错误信息 + * @param status 状态码 + * @param statusText 状态文本 + * @param data 错误数据 + */ + constructor(message: string, status: number, statusText: string, data: any) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.statusText = statusText; + this.data = data; + } +} + +/** + * HTTP客户端类 + */ +export class HttpClient { + private tokenGetter?: () => string | null; + private tenantId?: string; + + /** + * 构造函数 + * @param logout + * @param tokenGetter Token获取函数 + */ + constructor(tokenGetter?: () => string | null) { + this.tokenGetter = tokenGetter; + + } + + /** + * 设置Token获取函数 + * @param tokenGetter Token获取函数 + */ + setTokenGetter(tokenGetter: () => string | null): void { + this.tokenGetter = tokenGetter; + } + + /** + * 设置租户ID + * @param tenantId 租户ID + */ + setTenantId(tenantId?: string): void { + this.tenantId = tenantId; + } + + /** + * 发送HTTP请求 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async request(options: HttpRequestOptions): Promise> { + const { + method, + url, + headers = {}, + body, + needAuth = true + } = options; + + // 构建请求头 + const requestHeaders: Record = { + 'Content-Type': 'application/json', + ...headers + }; + + // 添加认证头 + const addAuthHeader = () => { + if (needAuth && this.tokenGetter) { + const token = this.tokenGetter(); + if (token) { + requestHeaders.Authorization = `${token}`; + } + } + }; + + // 添加租户ID头 + if (this.tenantId) { + requestHeaders['tenant-id'] = this.tenantId; + } + + addAuthHeader(); + + // 构建请求配置 + const fetchOptions: RequestInit = { + method, + headers: requestHeaders, + credentials: 'include' // 包含cookie + }; + + // 添加请求体 + if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { + fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body); + } + + try { + // 发送请求 + const response = await fetch(url, fetchOptions); + const responseData = await this.parseResponse(response); + + // 检查响应状态 + if (!response.ok) { + // 如果是401错误,尝试刷新Token并重试 + if (response.status === 401) { + return { + status: response.status, + statusText: response.statusText, + data: '' as T, + headers: this.parseHeaders(response.headers) + } + } + + // 其他错误,直接抛出 + const errorMsg = this.getErrorMessage(responseData); + throw new HttpError( + errorMsg, + response.status, + response.statusText, + responseData + ); + } + + // 处理成功响应的业务逻辑 + return this.handleResponse(response, responseData); + } catch (error) { + if (error instanceof HttpError) { + throw error; + } + + // 网络错误或其他错误 + throw new HttpError( + error instanceof Error ? error.message : 'Network Error', + 0, + 'Network Error', + null + ); + } + } + + + + /** + * 处理响应数据 + * @param response 响应对象 + * @param responseData 响应数据 + * @returns HttpResponse 处理后的响应 + */ + private handleResponse(response: Response, responseData: any): HttpResponse { + // 检查是否为业务响应结构 + if (this.isBusinessResponse(responseData)) { + // 业务响应结构:{ code, msg, data } + const { code, msg, data } = responseData; + + // 检查业务状态码 + if (code !== 0 && code !== 200 && code !== '0' && code !== '200') { + // 业务错误,抛出HttpError + throw new HttpError( + msg || `Business Error: ${code}`, + response.status, + response.statusText, + responseData + ); + } + + // 业务成功,返回data字段作为实际数据 + return { + status: response.status, + statusText: response.statusText, + data: data as T, + headers: this.parseHeaders(response.headers) + }; + } + + // 非业务响应结构,直接返回原始数据 + return { + status: response.status, + statusText: response.statusText, + data: responseData as T, + headers: this.parseHeaders(response.headers) + }; + } + + /** + * 检查是否为业务响应结构 + * @param responseData 响应数据 + * @returns boolean 是否为业务响应结构 + */ + private isBusinessResponse(responseData: any): boolean { + return typeof responseData === 'object' && + responseData !== null && + ('code' in responseData) && + ('msg' in responseData) && + ('data' in responseData); + } + + /** + * 获取错误信息 + * @param responseData 响应数据 + * @returns string 错误信息 + */ + private getErrorMessage(responseData: any): string { + // 如果是业务响应结构 + if (this.isBusinessResponse(responseData)) { + return responseData.msg || `Business Error: ${responseData.code}`; + } + + // 其他错误结构 + return responseData.message || responseData.error || `HTTP Error`; + } + + /** + * GET请求 + * @param url 请求URL + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async get(url: string, options?: Omit): Promise> { + return this.request({ + method: 'GET', + url, + ...options + }); + } + + /** + * POST请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async post(url: string, body?: any, options?: Omit): Promise> { + return this.request({ + method: 'POST', + url, + body, + ...options + }); + } + + /** + * PUT请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async put(url: string, body?: any, options?: Omit): Promise> { + return this.request({ + method: 'PUT', + url, + body, + ...options + }); + } + + /** + * DELETE请求 + * @param url 请求URL + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async delete(url: string, options?: Omit): Promise> { + return this.request({ + method: 'DELETE', + url, + ...options + }); + } + + /** + * PATCH请求 + * @param url 请求URL + * @param body 请求体 + * @param options 请求选项 + * @returns Promise> 响应结果 + */ + async patch(url: string, body?: any, options?: Omit): Promise> { + return this.request({ + method: 'PATCH', + url, + body, + ...options + }); + } + + /** + * 解析响应体 + * @param response 响应对象 + * @returns Promise 解析后的响应体 + */ + private async parseResponse(response: Response): Promise { + const contentType = response.headers.get('content-type') || ''; + + if (contentType.includes('application/json')) { + return response.json(); + } else if (contentType.includes('text/')) { + return response.text(); + } else { + return response.blob(); + } + } + + /** + * 解析响应头 + * @param headers 响应头对象 + * @returns Record 解析后的响应头 + */ + private parseHeaders(headers: Headers): Record { + const result: Record = {}; + headers.forEach((value, key) => { + result[key] = value; + }); + return result; + } +} diff --git a/sdk/frontend/oauth2-login-sdk/src/core/token.ts b/sdk/frontend/oauth2-login-sdk/src/core/token.ts new file mode 100644 index 0000000..bf353a6 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/core/token.ts @@ -0,0 +1,45 @@ +/** + * Token管理模块 + * 负责Token的存储、获取、刷新和过期处理 + */ + +import { Storage } from '../utils/storage'; + +/** + * Token管理类 + */ +export class TokenManager { + private storage: Storage; + + /** + * 构造函数 + * @param storage 存储实例 + * @param httpClient HTTP客户端实例 + */ + constructor(storage: Storage) { + this.storage = storage; + } + + /** + * 存储Token信息 + * @param tokenInfo Token信息 + */ + saveToken(tokenInfo: string): void { + this.storage.set('token', tokenInfo); + } + + /** + * 获取Token信息 + * @returns TokenInfo | null Token信息 + */ + getToken(): string | null { + return this.storage.get('token'); + } + + /** + * 清除Token信息 + */ + clearToken(): void { + this.storage.remove('token'); + } +} diff --git a/sdk/frontend/oauth2-login-sdk/src/guards/router.ts b/sdk/frontend/oauth2-login-sdk/src/guards/router.ts new file mode 100644 index 0000000..f1df6a6 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/guards/router.ts @@ -0,0 +1,130 @@ +/** + * 路由守卫模块 + * 提供基于权限的路由拦截和未登录自动跳转登录页功能 + */ + +import { Auth } from '../core/auth'; + +/** + * 路由守卫选项 + */ +export interface RouterGuardOptions { + /** + * 是否需要登录 + */ + requiresAuth?: boolean; + /** + * 需要的权限列表 + */ + requiredPermissions?: string[]; + /** + * 登录后重定向的URL + */ + redirectUri?: string; + /** + * 权限不足时重定向的URL + */ + unauthorizedRedirectUri?: string; +} + +/** + * 路由守卫类 + */ +export class RouterGuard { + private auth: Auth; + + /** + * 构造函数 + * @param auth 认证实例 + */ + constructor(auth: Auth) { + this.auth = auth; + } + + /** + * 检查路由权限 + * @param options 路由守卫选项 + * @returns Promise 是否通过权限检查 + */ + async check(options: RouterGuardOptions): Promise { + const { requiresAuth = true, requiredPermissions = [] } = options; + + // 检查是否需要登录 + if (requiresAuth) { + // 检查是否已认证 + if (!this.auth.isAuthenticated()) { + // 未认证,跳转到登录页 + this.auth.login(options.redirectUri); + return false; + } + + // 检查是否需要权限 + if (requiredPermissions.length > 0) { + // 获取用户权限 + const userPermissions = ['']; + + // 检查是否拥有所有需要的权限 + const hasPermission = requiredPermissions.every(permission => + userPermissions.includes(permission) + ); + + if (!hasPermission) { + // 权限不足,跳转到权限不足页 + if (options.unauthorizedRedirectUri) { + window.location.href = options.unauthorizedRedirectUri; + } + return false; + } + } + } + + return true; + } + + /** + * 创建Vue路由守卫 + * @returns 路由守卫函数 + */ + createVueGuard() { + return async (to: any, from: any, next: any) => { + // 从路由元信息中获取守卫选项 + const options: RouterGuardOptions = to.meta?.auth || {}; + + try { + const allowed = await this.check(options); + if (allowed) { + next(); + } + } catch (error) { + console.error('Route guard error:', error); + next(false); + } + }; + } + + /** + * 检查当前用户是否有权限访问资源 + * @param permissions 需要的权限列表 + * @returns Promise 是否拥有权限 + */ + async hasPermission(permissions: string | string[]): Promise { + if (!permissions) { + return true; + } + + const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions]; + + // 检查是否已认证 + if (!this.auth.isAuthenticated()) { + return false; + } + + // 获取用户权限 + const userPermissions = [''] + + // 检查是否拥有所有需要的权限 + return requiredPermissions.every(permission => + userPermissions.includes(permission) + ); + } +} diff --git a/sdk/frontend/oauth2-login-sdk/src/index.ts b/sdk/frontend/oauth2-login-sdk/src/index.ts new file mode 100644 index 0000000..08387bc --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/index.ts @@ -0,0 +1,94 @@ +/** + * 统一登录SDK入口文件 + * 支持OAuth2授权码模式,提供完整的Token管理和用户信息管理功能 + */ + +// 导出核心类和功能 +export { Auth } from './core/auth'; +export { TokenManager } from './core/token'; +export { HttpClient, HttpError } from './core/http'; +export { Storage } from './utils/storage'; +export { RouterGuard, RouterGuardOptions } from './guards/router'; + +// 导出工具函数 +export { + generateRandomString, + parseQueryParams, + buildQueryParams, + generateAuthorizationUrl, + isCallbackUrl +} from './utils/url'; + +// 导出类型定义 +export * from './types'; + +// 导出Vue插件 +export { VuePlugin, createVuePlugin } from './plugins/vue'; + +// 创建默认SDK实例 +import { SDKConfig, UnifiedLoginSDK } from './types'; +import { Auth as AuthCore } from './core/auth'; +import { Storage as StorageCore } from './utils/storage'; + +/** + * 默认SDK实例 + */ +const defaultStorage = new StorageCore(); +const defaultAuth = new AuthCore(defaultStorage); + +/** + * 默认导出的SDK实例 + */ +export const unifiedLoginSDK: UnifiedLoginSDK = { + init: (config: SDKConfig) => { + defaultAuth.init(config); + }, + getToken: () => { + return defaultAuth.getToken() + }, + login: (redirectUri?: string) => { + return defaultAuth.login(redirectUri); + }, + logout: () => { + return defaultAuth.logout(); + }, + handleCallback: () => { + return defaultAuth.handleCallback(); + }, + getRoutes: () => { + return defaultAuth.getRoutes(); + }, + getUserInfo: () => { + return defaultAuth.getUserInfo(); + }, + isAuthenticated: () => { + return defaultAuth.isAuthenticated(); + }, + hasRole: (role: string | string[]) => { + return defaultAuth.hasRole(role); + }, + hasAllRoles: (roles: string[]) => { + return defaultAuth.hasAllRoles(roles); + }, + hasPermission: (permission: string | string[]) => { + return defaultAuth.hasPermission(permission); + }, + hasAllPermissions: (permissions: string[]) => { + return defaultAuth.hasAllPermissions(permissions); + }, + on: (event, callback) => { + return defaultAuth.on(event, callback); + }, + off: (event, callback) => { + return defaultAuth.off(event, callback); + }, + isCallback: () => { + return defaultAuth.isCallback(); + } +}; + +// 默认导出 +export default unifiedLoginSDK; + +// 版本信息 +export const version = '1.0.0'; diff --git a/sdk/frontend/oauth2-login-sdk/src/plugins/vue.ts b/sdk/frontend/oauth2-login-sdk/src/plugins/vue.ts new file mode 100644 index 0000000..c736792 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/plugins/vue.ts @@ -0,0 +1,121 @@ +/** + * Vue插件模块 + * 提供Vue应用中使用统一登录SDK的能力 + */ + +import { Auth } from '../core/auth'; +import { SDKConfig } from '../types'; +import { Storage } from '../utils/storage'; +import { RouterGuard } from '../guards/router'; + +/** + * Vue插件选项 + */ +export interface VuePluginOptions { + /** + * SDK配置 + */ + config: SDKConfig; + /** + * 插件名称,默认'unifiedLogin' + */ + pluginName?: string; +} + +/** + * Vue插件类 + */ +export class VuePlugin { + private auth: Auth; + private routerGuard: RouterGuard; + + /** + * 构造函数 + * @param storage 存储实例 + */ + constructor(storage: Storage) { + this.auth = new Auth(storage); + this.routerGuard = new RouterGuard(this.auth); + } + + /** + * 安装Vue插件 + * @param app Vue构造函数或Vue 3应用实例 + * @param options 插件选项 + */ + install(app: any, options: VuePluginOptions): void { + const { config, pluginName = 'unifiedLogin' } = options; + + // 初始化SDK + this.auth.init(config); + + // 判断是Vue 2还是Vue 3 + const isVue3 = typeof app.config !== 'undefined'; + + if (isVue3) { + // Vue 3 + // 在全局属性上挂载SDK实例 + app.config.globalProperties[`${pluginName}`] = this.auth; + app.config.globalProperties.$auth = this.auth; // 兼容简写 + + // 提供Vue组件内的注入 + app.provide(pluginName, this.auth); + app.provide('auth', this.auth); // 兼容简写 + + // 处理路由守卫 + app.mixin({ + beforeCreate() { + // 如果是根组件,添加路由守卫 + if (this.$options.router) { + const router = this.$options.router; + // 添加全局前置守卫 + router.beforeEach(this.routerGuard.createVueGuard()); + } + } + }); + } else { + // Vue 2 + // 在Vue实例上挂载SDK实例 + app.prototype[`${pluginName}`] = this.auth; + app.prototype.$auth = this.auth; // 兼容简写 + + // 全局混入 + app.mixin({ + beforeCreate() { + // 如果是根组件,添加路由守卫 + if (this.$options.router) { + const router = this.$options.router; + // 添加全局前置守卫 + router.beforeEach(this.routerGuard.createVueGuard()); + } + } + }); + } + } + + /** + * 获取认证实例 + * @returns Auth 认证实例 + */ + getAuth(): Auth { + return this.auth; + } + + /** + * 获取路由守卫实例 + * @returns RouterGuard 路由守卫实例 + */ + getRouterGuard(): RouterGuard { + return this.routerGuard; + } +} + +/** + * 创建Vue插件实例 + * @param storageType 存储类型 + * @returns VuePlugin Vue插件实例 + */ +export function createVuePlugin(storageType?: 'localStorage' | 'sessionStorage' | 'cookie'): VuePlugin { + const storage = new Storage(storageType); + return new VuePlugin(storage); +} diff --git a/sdk/frontend/oauth2-login-sdk/src/types/config.ts b/sdk/frontend/oauth2-login-sdk/src/types/config.ts new file mode 100644 index 0000000..8c76dd8 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/types/config.ts @@ -0,0 +1,40 @@ +/** + * SDK配置选项 + */ +export interface SDKConfig { + /** 客户端ID */ + clientId: string; + /** 注册id **/ + registrationId: string, + /** 后端basepath路径*/ + basepath: string, + /** 存储类型,默认localStorage */ + storageType?: 'localStorage' | 'sessionStorage' | 'cookie'; + idpLogoutUrl:string; + homePage: string; + /** 租户ID(可选) */ + tenantId?: string; +} + +/** + * Token信息 + */ +export interface TokenInfo { + /** 访问令牌 */ + accessToken: string; + /** 刷新令牌 */ + refreshToken: string; + /** 令牌类型,默认Bearer */ + tokenType?: string; + /** 访问令牌过期时间(秒) */ + expiresIn: number; + /** 刷新令牌过期时间(秒) */ + refreshExpiresIn?: number; + /** 令牌颁发时间戳 */ + issuedAt: number; +} + +/** + * 事件类型 + */ +export type EventType = 'login' | 'logout' | 'tokenExpired'; diff --git a/sdk/frontend/oauth2-login-sdk/src/types/index.ts b/sdk/frontend/oauth2-login-sdk/src/types/index.ts new file mode 100644 index 0000000..beaf006 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/types/index.ts @@ -0,0 +1,94 @@ +import {RouterInfo} from "./user"; + +export * from './config'; +export * from './user'; + +/** + * 统一登录SDK接口 + */ +export interface UnifiedLoginSDK { + /** + * 初始化SDK配置 + * @param config SDK配置选项 + */ + init(config: import('./config').SDKConfig): void; + + getToken():string|null + /** + * 触发登录流程 + * @param redirectUri 可选的重定向URL,覆盖初始化时的配置 + */ + login(redirectUri?: string): Promise; + + /** + * 退出登录 + */ + logout(): Promise; + + /** + * 处理授权回调 + * @returns Promise 用户信息 + */ + handleCallback(): Promise; + getRoutes(): Promise; + + /** + * 获取用户信息 + * @returns Promise 用户信息 + */ + getUserInfo(): import('./user').UserInfo; + + /** + * 检查用户是否已认证 + * @returns boolean 是否已认证 + */ + isAuthenticated(): boolean; + + /** + * 检查用户是否有指定角色 + * @param role 角色编码或角色编码列表 + * @returns Promise 是否有指定角色 + */ + hasRole(role: string | string[]): Promise; + + /** + * 检查用户是否有所有指定角色 + * @param roles 角色编码列表 + * @returns Promise 是否有所有指定角色 + */ + hasAllRoles(roles: string[]): Promise; + + /** + * 检查用户是否有指定权限 + * @param permission 权限标识或权限标识列表 + * @returns Promise 是否有指定权限 + */ + hasPermission(permission: string | string[]): Promise; + + /** + * 检查用户是否有所有指定权限 + * @param permissions 权限标识列表 + * @returns Promise 是否有所有指定权限 + */ + hasAllPermissions(permissions: string[]): Promise; + + /** + * 事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + on(event: import('./config').EventType, callback: Function): void; + + /** + * 移除事件监听 + * @param event 事件类型 + * @param callback 回调函数 + */ + off(event: import('./config').EventType, callback: Function): void; + + /** + * 检查当前URL是否为授权回调 + * @returns boolean 是否为授权回调 + */ + isCallback(): boolean; +} diff --git a/sdk/frontend/oauth2-login-sdk/src/types/user.ts b/sdk/frontend/oauth2-login-sdk/src/types/user.ts new file mode 100644 index 0000000..ed23753 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/types/user.ts @@ -0,0 +1,88 @@ +/** + * 菜单信息 + */ +export interface RouterInfo { + /** 菜单名称 */ + name: string; + /** 菜单路径 */ + path?: string; + hidden: boolean; + redirect: string; + query: string; + alwaysShow: boolean; + /** 菜单组件 */ + component?: string; + meta:MetaVo; + children: RouterInfo; +} +export interface MetaVo { + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + title:string; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + icon:string; + + /** + * 设置为true,则不会被 缓存 + */ + noCache:boolean; + + /** + * 内链地址(http(s)://开头) + */ + link:string; +} + + +/** + * 用户基本信息 + */ +export interface UserInfo { + /** 用户ID */ + userId: string; + /** 用户名 */ + username: string; + /** 姓名 */ + nickName: string; + /** 邮箱 */ + currentDeptId: string; + /** 部门 */ + userDepts?: UserDept[]; + /** 岗位 */ + userPost?: UserPost[]; + /** 性别 */ + sex: string; + /** 用户角色 */ + roles?: string[]; + /** 权限列表 */ + permissions?: string[]; + dataPermission: DataPermission +} + +export interface DataPermission { + allowAll: boolean; + onlySelf: boolean; + deptList?: string[]; + areas?:string[] +} +export interface UserDept{ + postCode:string + postId:bigint + postName:string + postSort:bigint + remark:string + status:bigint +} +export interface UserPost{ + ancestors: string + deptId: bigint + deptName: string + leader: string + orderNum: bigint + parentId: bigint + status: bigint +} diff --git a/sdk/frontend/oauth2-login-sdk/src/utils/storage.ts b/sdk/frontend/oauth2-login-sdk/src/utils/storage.ts new file mode 100644 index 0000000..083eda0 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/utils/storage.ts @@ -0,0 +1,358 @@ +/** + * 存储工具类 + * 支持localStorage、sessionStorage和cookie三种存储方式 + */ + +type StorageType = 'localStorage' | 'sessionStorage' | 'cookie'; + +/** + * 存储工具类 + */ +export class Storage { + private storageType: StorageType; + private prefix: string; + + /** + * 构造函数 + * @param storageType 存储类型 + * @param prefix 存储前缀,默认'unified_login_' + */ + constructor(storageType: StorageType = 'localStorage', prefix: string = 'unified_login_') { + this.storageType = storageType; + this.prefix = prefix; + } + + /** + * 设置存储项 + * @param key 存储键 + * @param value 存储值 + * @param options 可选参数,cookie存储时使用 + */ + set(key: string, value: any, options?: { expires?: number; path?: string; domain?: string; secure?: boolean }): void { + const fullKey = this.prefix + key; + const stringValue = typeof value === 'string' ? value : JSON.stringify(value); + + switch (this.storageType) { + case 'localStorage': + this.setLocalStorage(fullKey, stringValue); + break; + case 'sessionStorage': + this.setSessionStorage(fullKey, stringValue); + break; + case 'cookie': + this.setCookie(fullKey, stringValue, options); + break; + } + } + + /** + * 获取存储项 + * @param key 存储键 + * @returns 存储值 + */ + get(key: string): any { + const fullKey = this.prefix + key; + let value: any; + + switch (this.storageType) { + case 'localStorage': + value = this.getLocalStorage(fullKey); + break; + case 'sessionStorage': + value = this.getSessionStorage(fullKey); + break; + case 'cookie': + value = this.getCookie(fullKey); + break; + default: + value = null; + } + + if (value === null) { + return null; + } + + // 尝试解析JSON + try { + return JSON.parse(value); + } catch (e) { + // 如果不是JSON,直接返回字符串 + return value; + } + } + + /** + * 移除存储项 + * @param key 存储键 + */ + remove(key: string): void { + const fullKey = this.prefix + key; + + switch (this.storageType) { + case 'localStorage': + this.removeLocalStorage(fullKey); + break; + case 'sessionStorage': + this.removeSessionStorage(fullKey); + break; + case 'cookie': + this.removeCookie(fullKey); + break; + } + } + + /** + * 清空所有存储项 + */ + clear(): void { + switch (this.storageType) { + case 'localStorage': + this.clearLocalStorage(); + break; + case 'sessionStorage': + this.clearSessionStorage(); + break; + case 'cookie': + this.clearCookie(); + break; + } + } + + /** + * 检查存储类型是否可用 + * @returns boolean 是否可用 + */ + isAvailable(): boolean { + try { + switch (this.storageType) { + case 'localStorage': + return this.isLocalStorageAvailable(); + case 'sessionStorage': + return this.isSessionStorageAvailable(); + case 'cookie': + return typeof document !== 'undefined'; + default: + return false; + } + } catch (e) { + return false; + } + } + + // ------------------------ localStorage 操作 ------------------------ + + /** + * 设置localStorage + */ + private setLocalStorage(key: string, value: string): void { + if (this.isLocalStorageAvailable()) { + localStorage.setItem(key, value); + } + } + + /** + * 获取localStorage + */ + private getLocalStorage(key: string): string | null { + if (this.isLocalStorageAvailable()) { + return localStorage.getItem(key); + } + return null; + } + + /** + * 移除localStorage + */ + private removeLocalStorage(key: string): void { + if (this.isLocalStorageAvailable()) { + localStorage.removeItem(key); + } + } + + /** + * 清空localStorage中所有带前缀的项 + */ + private clearLocalStorage(): void { + if (this.isLocalStorageAvailable()) { + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(this.prefix)) { + localStorage.removeItem(key); + i--; // 索引调整 + } + } + } + } + + /** + * 检查localStorage是否可用 + */ + private isLocalStorageAvailable(): boolean { + if (typeof localStorage === 'undefined') { + return false; + } + try { + const testKey = '__storage_test__'; + localStorage.setItem(testKey, testKey); + localStorage.removeItem(testKey); + return true; + } catch (e) { + return false; + } + } + + // ------------------------ sessionStorage 操作 ------------------------ + + /** + * 设置sessionStorage + */ + private setSessionStorage(key: string, value: string): void { + if (this.isSessionStorageAvailable()) { + sessionStorage.setItem(key, value); + } + } + + /** + * 获取sessionStorage + */ + private getSessionStorage(key: string): string | null { + if (this.isSessionStorageAvailable()) { + return sessionStorage.getItem(key); + } + return null; + } + + /** + * 移除sessionStorage + */ + private removeSessionStorage(key: string): void { + if (this.isSessionStorageAvailable()) { + sessionStorage.removeItem(key); + } + } + + /** + * 清空sessionStorage中所有带前缀的项 + */ + private clearSessionStorage(): void { + if (this.isSessionStorageAvailable()) { + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + if (key && key.startsWith(this.prefix)) { + sessionStorage.removeItem(key); + i--; // 索引调整 + } + } + } + } + + /** + * 检查sessionStorage是否可用 + */ + private isSessionStorageAvailable(): boolean { + if (typeof sessionStorage === 'undefined') { + return false; + } + try { + const testKey = '__storage_test__'; + sessionStorage.setItem(testKey, testKey); + sessionStorage.removeItem(testKey); + return true; + } catch (e) { + return false; + } + } + + // ------------------------ cookie 操作 ------------------------ + + /** + * 设置cookie + */ + private setCookie( + key: string, + value: string, + options?: { expires?: number; path?: string; domain?: string; secure?: boolean } + ): void { + if (typeof document === 'undefined') { + return; + } + + let cookieString = `${key}=${encodeURIComponent(value)}`; + + if (options) { + // 设置过期时间(秒) + if (options.expires) { + const date = new Date(); + date.setTime(date.getTime() + options.expires * 1000); + cookieString += `; expires=${date.toUTCString()}`; + } + + // 设置路径 + if (options.path) { + cookieString += `; path=${options.path}`; + } + + // 设置域名 + if (options.domain) { + cookieString += `; domain=${options.domain}`; + } + + // 设置secure + if (options.secure) { + cookieString += '; secure'; + } + } + + document.cookie = cookieString; + } + + /** + * 获取cookie + */ + private getCookie(key: string): string | null { + if (typeof document === 'undefined') { + return null; + } + + const name = `${key}=`; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + + return null; + } + + /** + * 移除cookie + */ + private removeCookie(key: string): void { + this.setCookie(key, '', { expires: -1 }); + } + + /** + * 清空所有带前缀的cookie + */ + private clearCookie(): void { + if (typeof document === 'undefined') { + return; + } + + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const eqPos = cookie.indexOf('='); + const key = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + if (key.startsWith(this.prefix)) { + this.removeCookie(key); + } + } + } +} diff --git a/sdk/frontend/oauth2-login-sdk/src/utils/url.ts b/sdk/frontend/oauth2-login-sdk/src/utils/url.ts new file mode 100644 index 0000000..d27f52b --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/src/utils/url.ts @@ -0,0 +1,125 @@ +/** + * URL处理工具 + * 用于生成授权URL、解析URL参数等功能 + */ + +/** + * 生成随机字符串 + * @param length 字符串长度,默认32位 + * @returns 随机字符串 + */ +export function generateRandomString(length: number = 32): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +/** + * 解析URL查询参数 + * @param url URL字符串,默认为当前URL + * @returns 查询参数对象 + */ +export function parseQueryParams(url: string = window.location.href): Record { + const params: Record = {}; + const queryString = url.split('?')[1]; + if (!queryString) { + return params; + } + + const pairs = queryString.split('&'); + for (const pair of pairs) { + const [key, value] = pair.split('='); + if (key) { + params[decodeURIComponent(key)] = decodeURIComponent(value || ''); + } + } + + return params; +} + +/** + * 构建URL查询参数 + * @param params 查询参数对象 + * @returns 查询参数字符串 + */ +export function buildQueryParams(params: Record): string { + const pairs: string[] = []; + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); + } + } + return pairs.length ? `?${pairs.join('&')}` : ''; +} + +/** + * 生成OAuth2授权URL + * @param authorizationEndpoint 授权端点URL + * @param clientId 客户端ID + * @param redirectUri 重定向URL + * @param options 可选参数 + * @returns 授权URL + */ +export function generateAuthorizationUrl( + authorizationEndpoint: string, + clientId: string, + redirectUri: string, + options?: { + responseType?: string; + scope?: string; + state?: string; + [key: string]: any; + } +): string { + const { + responseType = 'code', + scope, + state = generateRandomString(32), + ...extraParams + } = options || {}; + + const params = { + client_id: clientId, + redirect_uri: redirectUri, + response_type: responseType, + state, + ...(scope ? { scope } : {}), + ...extraParams + }; + + const queryString = buildQueryParams(params); + return `${authorizationEndpoint}${queryString}`; +} + +/** + * 检查当前URL是否为授权回调 + * @param url URL字符串,默认为当前URL + * @returns 是否为授权回调 + */ +export function isCallbackUrl(url: string = window.location.href): boolean { + const params = parseQueryParams(url); + return !!params.code || !!params.error; +} + +/** + * 获取当前URL的路径名 + * @param url URL字符串,默认为当前URL + * @returns 路径名 + */ +export function getPathname(url: string = window.location.href): string { + const urlObj = new URL(url); + return urlObj.pathname; +} + +/** + * 获取当前URL的主机名 + * @param url URL字符串,默认为当前URL + * @returns 主机名 + */ +export function getHostname(url: string = window.location.href): string { + const urlObj = new URL(url); + return urlObj.hostname; +} diff --git a/sdk/frontend/oauth2-login-sdk/tsconfig.json b/sdk/frontend/oauth2-login-sdk/tsconfig.json new file mode 100644 index 0000000..dd7b863 --- /dev/null +++ b/sdk/frontend/oauth2-login-sdk/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "ESNext", + "moduleResolution": "node", + "lib": ["ES2018", "DOM"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +}