t
This commit is contained in:
commit
ff528b7017
268 changed files with 336913 additions and 0 deletions
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
@ -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/
|
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
2
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
2
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
5
create.sql
Normal file
5
create.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
create table analytics (redirection_time float(53), analytics_id bigint not null auto_increment, url_shortener_id bigint not null, accept_language varchar(255), accept_types varchar(255), device_ip varchar(255), device_type varchar(255), timestamp varchar(255), timezone varchar(255), primary key (analytics_id)) engine=InnoDB;
|
||||
create table url_shortener (created_at DATE, hits bigint, id bigint not null auto_increment, unique_hits bigint, user_id bigint, qr_code varchar(2048), bio_content TEXT, link_type varchar(255), original_url varchar(255), short_url varchar(255), primary key (id)) engine=InnoDB;
|
||||
create table user_entity (account_non_expired bit, account_non_locked bit, credentials_non_expired bit, enabled bit, user_id bigint not null auto_increment, email varchar(255) not null, first_name varchar(255), last_name varchar(255), password varchar(255) not null, username varchar(255) not null, primary key (user_id)) engine=InnoDB;
|
||||
alter table user_entity add constraint UK_2jsk4eakd0rmvybo409wgwxuw unique (username);
|
||||
alter table analytics add constraint FK1imt48hd0qslhxwt2gcoh92h1 foreign key (url_shortener_id) references url_shortener (id);
|
BIN
image.png
Normal file
BIN
image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
303578
logs/shortener.log
Normal file
303578
logs/shortener.log
Normal file
File diff suppressed because it is too large
Load diff
308
mvnw
vendored
Normal file
308
mvnw
vendored
Normal file
|
@ -0,0 +1,308 @@
|
|||
#!/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
|
||||
#
|
||||
# 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.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "$(uname)" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||
else
|
||||
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=$(java-config --jre-home)
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="$(which javac)"
|
||||
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=$(which readlink)
|
||||
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||
else
|
||||
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||
fi
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
# Remove \r in case we run on Windows within Git Bash
|
||||
# and check out the repository with auto CRLF management
|
||||
# enabled. Otherwise, we may read lines that are delimited with
|
||||
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
|
||||
# splitting rules.
|
||||
tr -s '\r\n' ' ' < "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
log() {
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
printf '%s\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
log "$MAVEN_PROJECTBASEDIR"
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if [ -r "$wrapperJarPath" ]; then
|
||||
log "Found $wrapperJarPath"
|
||||
else
|
||||
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
else
|
||||
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
fi
|
||||
while IFS="=" read -r key value; do
|
||||
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
|
||||
safeValue=$(echo "$value" | tr -d '\r')
|
||||
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
log "Downloading from: $wrapperUrl"
|
||||
|
||||
if $cygwin; then
|
||||
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
log "Found wget ... using wget"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
log "Found curl ... using curl"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
else
|
||||
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
else
|
||||
log "Falling back to using Java to download"
|
||||
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaSource=$(cygpath --path --windows "$javaSource")
|
||||
javaClass=$(cygpath --path --windows "$javaClass")
|
||||
fi
|
||||
if [ -e "$javaSource" ]; then
|
||||
if [ ! -e "$javaClass" ]; then
|
||||
log " - Compiling MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
log " - Running MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
wrapperSha256Sum=""
|
||||
while IFS="=" read -r key value; do
|
||||
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ -n "$wrapperSha256Sum" ]; then
|
||||
wrapperSha256Result=false
|
||||
if command -v sha256sum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
elif command -v shasum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
|
||||
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
|
||||
exit 1
|
||||
fi
|
||||
if [ $wrapperSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
|
||||
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
# shellcheck disable=SC2086 # safe args
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
205
mvnw.cmd
vendored
Normal file
205
mvnw.cmd
vendored
Normal file
|
@ -0,0 +1,205 @@
|
|||
@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 https://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.2.0
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %WRAPPER_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
SET WRAPPER_SHA_256_SUM=""
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
|
||||
)
|
||||
IF NOT %WRAPPER_SHA_256_SUM%=="" (
|
||||
powershell -Command "&{"^
|
||||
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" exit 1;"^
|
||||
"}"^
|
||||
"}"
|
||||
if ERRORLEVEL 1 goto error
|
||||
)
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath %WRAPPER_JAR% ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||
|
||||
cmd /C exit /B %ERROR_CODE%
|
222
pom.xml
Normal file
222
pom.xml
Normal file
|
@ -0,0 +1,222 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.bitmutex</groupId>
|
||||
<artifactId>shortener</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>URLShortener</name>
|
||||
<description>URLShortener</description>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!--QRCODE -->
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>3.4.1</version> <!-- Replace with the latest version -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>javase</artifactId>
|
||||
<version>3.4.1</version> <!-- Replace with the latest version -->
|
||||
</dependency>
|
||||
<!--QRCODE -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.2.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>32.0.0-android</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT-->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>0.9.1</version> <!-- or the latest version -->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>RELEASE</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.webjars.npm</groupId>
|
||||
<artifactId>axios</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.razorpay/razorpay-java -->
|
||||
<dependency>
|
||||
<groupId>com.razorpay</groupId>
|
||||
<artifactId>razorpay-java</artifactId>
|
||||
<version>1.4.4</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testng</groupId>
|
||||
<artifactId>testng</artifactId>
|
||||
<version>7.4.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||
<version>2.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<configuration>
|
||||
<suiteXmlFiles>
|
||||
<suiteXmlFile>testng.xml</suiteXmlFile>
|
||||
</suiteXmlFiles>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-maven-plugin</artifactId>
|
||||
<version>1.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>integration-test</phase>
|
||||
<goals>
|
||||
<goal>generate</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>2.3.3.RELEASE</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>pre-integration-test</id>
|
||||
<goals>
|
||||
<goal>start</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>post-integration-test</id>
|
||||
<goals>
|
||||
<goal>stop</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
18313
shortener_db_schema.sql
Normal file
18313
shortener_db_schema.sql
Normal file
File diff suppressed because it is too large
Load diff
120
src/main/java/com/bitmutex/shortener/Analytics.java
Normal file
120
src/main/java/com/bitmutex/shortener/Analytics.java
Normal file
|
@ -0,0 +1,120 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(name = "analytics")
|
||||
public class Analytics {
|
||||
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public UrlShortener getUrlShortener() {
|
||||
return urlShortener;
|
||||
}
|
||||
|
||||
public void setUrlShortener(UrlShortener urlShortener) {
|
||||
this.urlShortener = urlShortener;
|
||||
}
|
||||
|
||||
public String getDeviceIp() {
|
||||
return deviceIp;
|
||||
}
|
||||
|
||||
public void setDeviceIp(String deviceIp) {
|
||||
this.deviceIp = deviceIp;
|
||||
}
|
||||
|
||||
public String getTimezone() {
|
||||
return timezone;
|
||||
}
|
||||
|
||||
public void setTimezone(String timezone) {
|
||||
this.timezone = timezone;
|
||||
}
|
||||
|
||||
public String getDeviceType() {
|
||||
return deviceType;
|
||||
}
|
||||
|
||||
public void setDeviceType(String deviceType) {
|
||||
this.deviceType = deviceType;
|
||||
}
|
||||
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "analytics_id")
|
||||
private Long id;
|
||||
|
||||
// In UrlAnalytics class
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "url_shortener_id", nullable = false)
|
||||
@JsonIgnore
|
||||
private UrlShortener urlShortener;
|
||||
|
||||
@Column(name = "device_ip")
|
||||
private String deviceIp;
|
||||
|
||||
@Column(name = "timezone")
|
||||
private String timezone;
|
||||
|
||||
@Column(name = "device_type")
|
||||
private String deviceType;
|
||||
|
||||
public String getAcceptTypes() {
|
||||
return acceptTypes;
|
||||
}
|
||||
|
||||
public void setAcceptTypes(String acceptTypes) {
|
||||
this.acceptTypes = acceptTypes;
|
||||
}
|
||||
|
||||
public String getAcceptLanguage() {
|
||||
return acceptLanguage;
|
||||
}
|
||||
|
||||
public void setAcceptLanguage(String acceptLanguage) {
|
||||
this.acceptLanguage = acceptLanguage;
|
||||
}
|
||||
|
||||
@Column(name = "accept_language")
|
||||
private String acceptLanguage;
|
||||
@Column(name = "accept_types")
|
||||
private String acceptTypes;
|
||||
|
||||
public String getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(String timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Column(name = "timestamp")
|
||||
private String timestamp;
|
||||
|
||||
public Double getRedirectionTime() {
|
||||
return redirectionTime;
|
||||
}
|
||||
|
||||
public void SetRedirectionTime(Double redirectionTime) {
|
||||
this.redirectionTime = redirectionTime;
|
||||
}
|
||||
|
||||
@Column(name = "redirection_time")
|
||||
private Double redirectionTime;
|
||||
|
||||
|
||||
// Other analytics data fields...
|
||||
|
||||
// Getters and setters...
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// AnalyticsController.java
|
||||
|
||||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
public class AnalyticsController {
|
||||
|
||||
@Autowired
|
||||
private AnalyticsService analyticsService;
|
||||
|
||||
private AnalyticsRepository analyticsRepository;
|
||||
|
||||
@Autowired
|
||||
private UrlShortenerService urlShortenerService;
|
||||
|
||||
@GetMapping("/api/url/analytics")
|
||||
@ResponseBody
|
||||
public List<Analytics> getAnalyticsData(@RequestParam String shortUrl) {
|
||||
UrlShortener urlShortener = urlShortenerService.findByShortUrl(shortUrl);
|
||||
|
||||
if (urlShortener != null) {
|
||||
Long urlShortenerId = urlShortener.getId();
|
||||
return analyticsService.findByUrlShortenerId(urlShortenerId);
|
||||
} else {
|
||||
return List.of(); // Return an empty list if the short URL is not found
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@Repository
|
||||
public interface AnalyticsRepository extends JpaRepository<Analytics, Long> {
|
||||
// Add custom query methods if needed
|
||||
List<Analytics> findByDeviceIp(String deviceIp);
|
||||
long countByUrlShortenerId(Long urlShortenerId);
|
||||
List<Analytics> findByUrlShortenerId(Long urlShortenerId);
|
||||
|
||||
}
|
23
src/main/java/com/bitmutex/shortener/AnalyticsService.java
Normal file
23
src/main/java/com/bitmutex/shortener/AnalyticsService.java
Normal file
|
@ -0,0 +1,23 @@
|
|||
// AnalyticsService.java
|
||||
|
||||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class AnalyticsService {
|
||||
|
||||
@Autowired
|
||||
private AnalyticsRepository analyticsRepository;
|
||||
|
||||
public List<Analytics> findByUrlShortenerId(Long urlShortenerId) {
|
||||
// Implement this method to fetch analytics entries based on urlShortenerId
|
||||
return analyticsRepository.findByUrlShortenerId(urlShortenerId);
|
||||
}
|
||||
|
||||
}
|
27
src/main/java/com/bitmutex/shortener/BioController.java
Normal file
27
src/main/java/com/bitmutex/shortener/BioController.java
Normal file
|
@ -0,0 +1,27 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
// BioController.java
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Controller
|
||||
|
||||
public class BioController {
|
||||
|
||||
private final UrlShortenerService urlShortenerService;
|
||||
|
||||
@Autowired
|
||||
public BioController(UrlShortenerService urlShortenerService) {
|
||||
this.urlShortenerService = urlShortenerService;
|
||||
}
|
||||
|
||||
@GetMapping("/bio")
|
||||
public String showBioEditPage() {
|
||||
return "bio";
|
||||
}
|
||||
}
|
90
src/main/java/com/bitmutex/shortener/ContactController.java
Normal file
90
src/main/java/com/bitmutex/shortener/ContactController.java
Normal file
|
@ -0,0 +1,90 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/contact")
|
||||
public class ContactController {
|
||||
|
||||
@Autowired
|
||||
ContactRepository contactRepository;
|
||||
|
||||
@Autowired
|
||||
private SmsService smsService;
|
||||
|
||||
@Autowired
|
||||
private EmailService emailService;
|
||||
|
||||
@GetMapping
|
||||
public String showContactForm() {
|
||||
return "contact";
|
||||
}
|
||||
|
||||
@PostMapping("/submitContactForm")
|
||||
public String submitContactForm(
|
||||
@RequestParam String name,
|
||||
@RequestParam String email,
|
||||
@RequestParam(required = false) String phoneNumber,
|
||||
@RequestParam SourceType sourceType,
|
||||
@RequestPart (required = false) MultipartFile file,
|
||||
@RequestParam String message,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
|
||||
try {
|
||||
// Validate and process file attachment
|
||||
if (file == null || file.isEmpty() || file.getSize() <= 2 * 1024 * 1024) {
|
||||
byte[] attachment = (file != null) ? file.getBytes() : null;
|
||||
|
||||
// Save the form data to the database
|
||||
ContactMessage contactMessage = new ContactMessage(name, email, phoneNumber, sourceType, attachment, message);
|
||||
contactRepository.save(contactMessage);
|
||||
|
||||
// Send email to admin with the form content
|
||||
String adminSubject = "Thank you for contacting us!";
|
||||
String adminMessage = "<html><body><p>New contact form submission:</p><table border=\"1\">" +
|
||||
"<tr><td><b>Name:</b></td><td>" + name + "</td></tr>" +
|
||||
"<tr><td><b>Email:</b></td><td>" + email + "</td></tr>" +
|
||||
"<tr><td><b>Phone Number:</b></td><td>" + phoneNumber + "</td></tr>" +
|
||||
"<tr><td><b>Source Type:</b></td><td>" + sourceType + "</td></tr>" +
|
||||
"<tr><td><b>Message:</b></td><td>" + message + "</td></tr>" +
|
||||
"</table></body></html>";
|
||||
|
||||
emailService.sendMail(email, adminSubject, adminMessage);
|
||||
|
||||
// Send SMS only if phone number is present and not null
|
||||
if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
smsService.sendSms(phoneNumber,"Thank you for contacting us! Your message has been received. We will contact you soon");
|
||||
}
|
||||
|
||||
// Add success flash attribute
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Form submitted successfully!");
|
||||
|
||||
return "redirect:/contact";
|
||||
} else {
|
||||
// Add error flash attribute for file size exceeded
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "File size should be less than 2MB.");
|
||||
|
||||
return "redirect:/contact";
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Handle exception
|
||||
// Add error flash attribute for file processing error
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Error processing file.");
|
||||
|
||||
return "redirect:/contact";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
43
src/main/java/com/bitmutex/shortener/ContactMessage.java
Normal file
43
src/main/java/com/bitmutex/shortener/ContactMessage.java
Normal file
|
@ -0,0 +1,43 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "contact_form")
|
||||
public class ContactMessage {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
private String phoneNumber;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private SourceType sourceType;
|
||||
|
||||
@Lob
|
||||
private byte[] attachment;
|
||||
|
||||
@Column(length = 2000)
|
||||
private String message;
|
||||
|
||||
// constructors, getters, setters...
|
||||
|
||||
// Additional code...
|
||||
|
||||
public ContactMessage(String name, String email, String phoneNumber, SourceType sourceType, byte[] attachment, String message) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.sourceType = sourceType;
|
||||
this.attachment = attachment;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ContactMessage() {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface ContactRepository extends JpaRepository<ContactMessage, Long> {
|
||||
// You can add custom query methods if needed
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class CustomHealthIndicator extends AbstractHealthIndicator {
|
||||
|
||||
@Autowired
|
||||
StatusCheckService statusCheckService;
|
||||
|
||||
public CustomHealthIndicator(StatusCheckService statusCheckService) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doHealthCheck(Health.Builder builder) {
|
||||
// Add custom health check logic here
|
||||
int isCustomComponentHealthy = performCustomCheck();
|
||||
|
||||
if (isCustomComponentHealthy == 1 ) {
|
||||
builder.up().withDetail("message", "Internal Custom status check component is healthy");
|
||||
} else {
|
||||
builder.down().withDetail("message", "Internal Custom status check component NOT healthy");
|
||||
}
|
||||
}
|
||||
|
||||
private int performCustomCheck() {
|
||||
Map<String, Integer> myMap = statusCheckService.getServerStatus();
|
||||
return myMap.get("server_status");
|
||||
}
|
||||
}
|
55
src/main/java/com/bitmutex/shortener/CustomOAuth2User.java
Normal file
55
src/main/java/com/bitmutex/shortener/CustomOAuth2User.java
Normal file
|
@ -0,0 +1,55 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
|
||||
public class CustomOAuth2User implements OAuth2User {
|
||||
|
||||
private OAuth2User oauth2User;
|
||||
|
||||
public CustomOAuth2User(OAuth2User oauth2User) {
|
||||
this.oauth2User = oauth2User;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return oauth2User.getAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return oauth2User.getAuthorities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return oauth2User.getAttribute("login");
|
||||
}
|
||||
|
||||
// Additional methods to get custom user details
|
||||
|
||||
public String getEmail() {
|
||||
return (String) oauth2User.getAttribute("email");
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
// Extract first name from GitHub attributes
|
||||
return (String) oauth2User.getAttribute("name");
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
// Extract last name from GitHub attributes
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
// Extract last name from GitHub attributes
|
||||
return oauth2User.getAttribute("login");
|
||||
}
|
||||
|
||||
// Add more methods as needed to retrieve other user details
|
||||
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
|
||||
@Autowired
|
||||
UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
UserService userService;
|
||||
|
||||
@Override
|
||||
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
|
||||
OAuth2User user = super.loadUser(userRequest);
|
||||
|
||||
|
||||
Map<String, Object> attributes = user.getAttributes();
|
||||
|
||||
System.out.println(attributes.toString());
|
||||
|
||||
String username = (String) attributes.get("login");
|
||||
System.out.println("Username:" + username);
|
||||
loadUserByOAuth2UserRequest(userRequest);
|
||||
|
||||
return new CustomOAuth2User(user);
|
||||
}
|
||||
public CustomUserDetails loadUserByOAuth2UserRequest(OAuth2UserRequest oAuth2UserRequest) {
|
||||
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
|
||||
|
||||
Map<String, Object> attributes = oAuth2User.getAttributes();
|
||||
String username = (String) attributes.get("login"); // Adjust based on the provider
|
||||
System.out.println(username);
|
||||
|
||||
// Extract other attributes as needed
|
||||
|
||||
Optional<UserEntity> userOptional = userRepository.findByUsername(username);
|
||||
UserEntity user;
|
||||
|
||||
if (userOptional.isPresent()) {
|
||||
user = userOptional.get();
|
||||
} else {
|
||||
RegistrationRequest registrationRequest = new RegistrationRequest();
|
||||
registrationRequest.setUsername(username);
|
||||
registrationRequest.setEmail(username+"@youremail.tld");
|
||||
registrationRequest.setFirstName(username);
|
||||
registrationRequest.setLastName(username);
|
||||
registrationRequest.setPassword(username+ randAlphaString() );
|
||||
//todo
|
||||
user = registerNewOAuth2User(registrationRequest);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return new CustomUserDetails(
|
||||
user.getUsername(),
|
||||
"", // You may need to adjust this if your OAuth2 login doesn't have a password
|
||||
true, // Assuming OAuth2 login is always enabled
|
||||
true, // Assuming OAuth2 login is never locked
|
||||
true, // Assuming OAuth2 login credentials never expire
|
||||
user.getEmail(),
|
||||
user.getFirstName(),
|
||||
user.getLastName(),
|
||||
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")),
|
||||
user.getId()
|
||||
);
|
||||
}
|
||||
|
||||
private UserEntity registerNewOAuth2User(RegistrationRequest request) {
|
||||
// Implement logic to register a new user based on OAuth2 attributes
|
||||
UserEntity newUser = userService.registerNewUser(request);
|
||||
return newUser;
|
||||
}
|
||||
|
||||
public String randAlphaString() {
|
||||
int leftLimit = 48; // numeral '0'
|
||||
int rightLimit = 122; // letter 'z'
|
||||
int targetStringLength = 10;
|
||||
Random random = new Random();
|
||||
|
||||
String generatedString = random.ints(leftLimit, rightLimit + 1)
|
||||
.filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
|
||||
.limit(targetStringLength)
|
||||
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
|
||||
.toString();
|
||||
|
||||
return generatedString;
|
||||
}
|
||||
}
|
38
src/main/java/com/bitmutex/shortener/CustomUserDetails.java
Normal file
38
src/main/java/com/bitmutex/shortener/CustomUserDetails.java
Normal file
|
@ -0,0 +1,38 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
public class CustomUserDetails extends org.springframework.security.core.userdetails.User {
|
||||
private final Long userId;
|
||||
private final String email;
|
||||
private final String firstName;
|
||||
private final String lastName;
|
||||
public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonLocked, boolean credentialsNonExpired, String email, String firstName, String lastName, Collection<? extends GrantedAuthority> authorities, Long userId ) {
|
||||
super(username, password, enabled, true, true, accountNonLocked, authorities);
|
||||
this.email = email;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public class DuplicateEmailException extends RuntimeException {
|
||||
|
||||
public DuplicateEmailException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DuplicateEmailException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
package com.bitmutex.shortener;
|
||||
|
||||
public class DuplicatePhoneNumberException extends RuntimeException {
|
||||
|
||||
public DuplicatePhoneNumberException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DuplicatePhoneNumberException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
46
src/main/java/com/bitmutex/shortener/EmailService.java
Normal file
46
src/main/java/com/bitmutex/shortener/EmailService.java
Normal file
|
@ -0,0 +1,46 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
|
||||
import com.nimbusds.openid.connect.sdk.assurance.evidences.attachment.Attachment;
|
||||
import jakarta.mail.MessagingException;
|
||||
import org.checkerframework.checker.units.qual.A;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class EmailService {
|
||||
|
||||
@Autowired
|
||||
private JavaMailSender javaMailSender;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String senderEmail;
|
||||
|
||||
public void sendMail(String to, String subject, String message) {
|
||||
|
||||
try {
|
||||
MimeMessageHelper helper = new MimeMessageHelper(javaMailSender.createMimeMessage(), true);
|
||||
helper.setFrom(senderEmail);
|
||||
helper.setTo(to);
|
||||
helper.setSubject(subject);
|
||||
|
||||
|
||||
// Construct the HTML content with header and footer
|
||||
String htmlContent = "<html><body>"
|
||||
+ "<header>" + "<h1>LinkShortener</h1>" + "</header>" // Customize as needed
|
||||
+ message // Include the main message content
|
||||
+ "<footer>" + "<p>© The LinkShortener Company 2023</p>" + "</footer>" // Customize as needed
|
||||
+ "</body></html>";
|
||||
|
||||
helper.setText(htmlContent, true); // Set as HTML content
|
||||
|
||||
javaMailSender.send(helper.getMimeMessage());
|
||||
} catch (MessagingException e) {
|
||||
// Handle email sending errors appropriately
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
@Controller
|
||||
public class ErrorPageController implements ErrorController {
|
||||
|
||||
@RequestMapping("/error")
|
||||
public String handleError(Model model) {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
|
||||
if (requestAttributes instanceof ServletRequestAttributes) {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
|
||||
// Object status = request.getAttribute("jakarta.servlet.error.status_code");
|
||||
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||
|
||||
if (status != null) {
|
||||
int statusCode = Integer.parseInt(status.toString());
|
||||
String statusMessage = request.getAttribute(RequestDispatcher.ERROR_MESSAGE).toString();
|
||||
String requestUri = request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI).toString();
|
||||
String servletName = request.getAttribute(RequestDispatcher.ERROR_SERVLET_NAME).toString();
|
||||
|
||||
model.addAttribute("errorCode", statusCode);
|
||||
model.addAttribute("errorMessage", statusMessage);
|
||||
model.addAttribute("requestUri", requestUri);
|
||||
model.addAttribute("servletName", servletName);
|
||||
|
||||
|
||||
// Customize error title, code, and message based on status code
|
||||
switch (statusCode) {
|
||||
case 404:
|
||||
model.addAttribute("errorTitle", "Page Not Found");
|
||||
break;
|
||||
case 500:
|
||||
model.addAttribute("errorTitle", "Internal Server Error");
|
||||
|
||||
break;
|
||||
// Add more cases as needed
|
||||
default:
|
||||
model.addAttribute("errorTitle", "Error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default error handling
|
||||
return "error";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/forgot-password")
|
||||
public class ForgotPasswordController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public ForgotPasswordController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
|
||||
@Autowired
|
||||
private JavaMailSender javaMailSender;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String senderEmail;
|
||||
|
||||
|
||||
@GetMapping
|
||||
public String showForgotPasswordForm() {
|
||||
return "forgot-password";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String processForgotPassword(@RequestParam("email") String email, Model model, HttpServletRequest request) {
|
||||
UserEntity user = userService.findByEmail(email);
|
||||
|
||||
if (user != null) {
|
||||
// Generate a reset token and set its expiration time
|
||||
String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
|
||||
String resetToken = UUID.randomUUID().toString();
|
||||
LocalDateTime resetTokenExpiryDateTime = LocalDateTime.now().plusHours(24);
|
||||
user.setResetToken(resetToken);
|
||||
user.setResetTokenExpiryDateTime(resetTokenExpiryDateTime);
|
||||
userService.save(user);
|
||||
|
||||
// Send an email to the user with the reset link
|
||||
sendResetPasswordEmail(user.getEmail(), user.getResetToken(), baseUrl);
|
||||
|
||||
|
||||
// For simplicity, let's assume the email is sent successfully
|
||||
return "redirect:/forgot-password?success";
|
||||
} else {
|
||||
model.addAttribute("error", "User not found");
|
||||
return "forgot-password";
|
||||
}
|
||||
}
|
||||
|
||||
private void sendResetPasswordEmail(String toEmail, String resetToken, String baseUrl) {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(senderEmail);
|
||||
message.setTo(toEmail);
|
||||
message.setSubject("Reset Your Password");
|
||||
String resetLink = baseUrl + "/reset-password?token=" + resetToken;
|
||||
message.setText("To reset your password, click the link below:\n" + resetLink);
|
||||
javaMailSender.send(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/forgot-username")
|
||||
public class ForgotUsernameController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public ForgotUsernameController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
|
||||
@Autowired
|
||||
private JavaMailSender javaMailSender;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String senderEmail;
|
||||
|
||||
@GetMapping
|
||||
public String showForgotUsernameForm() {
|
||||
return "forgot-username";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String processForgotUsername(@RequestParam("email") String email, Model model, HttpServletRequest request) {
|
||||
UserEntity user = userService.findByEmail(email);
|
||||
String loginUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/login";
|
||||
|
||||
if (user != null) {
|
||||
// Send an email to the user with their username
|
||||
sendForgotUsernameEmail(user.getEmail(), user.getUsername(), loginUrl);
|
||||
|
||||
// For simplicity, let's assume the email is sent successfully
|
||||
return "redirect:/forgot-username?success";
|
||||
} else {
|
||||
model.addAttribute("error", "User not found");
|
||||
return "forgot-username";
|
||||
}
|
||||
}
|
||||
|
||||
public void sendForgotUsernameEmail(String to, String username, String loginUrl) {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(senderEmail);
|
||||
message.setTo(to);
|
||||
message.setSubject("Forgot Username");
|
||||
String messageBody = "Your username is: " + username + "\nPlease login via this link :" + loginUrl;
|
||||
message.setText(messageBody);
|
||||
javaMailSender.send(message);
|
||||
}
|
||||
}
|
42
src/main/java/com/bitmutex/shortener/HomeController.java
Normal file
42
src/main/java/com/bitmutex/shortener/HomeController.java
Normal file
|
@ -0,0 +1,42 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
@RequestMapping("/")
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@GetMapping("/")
|
||||
String home(Authentication authentication, Model model) {
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
|
||||
String username = authentication.getName();
|
||||
|
||||
System.out.println("YOUR USERNAME FROM AUTH CONTEXT"+ username);
|
||||
|
||||
UserDetailsDto user = userService.getUserDetailsByUsername(username);
|
||||
UserEntity userElevated = userService.findByEmail(user.getEmail());
|
||||
|
||||
System.out.println("YOUR USERNAME FROM USERENTITY CONTEXT"+ user.getUsername());
|
||||
|
||||
model.addAttribute("user", user);
|
||||
model.addAttribute("username", user.getUsername());
|
||||
model.addAttribute("userelevated", userElevated);
|
||||
}
|
||||
return "index";
|
||||
}
|
||||
}
|
57
src/main/java/com/bitmutex/shortener/ImageController.java
Normal file
57
src/main/java/com/bitmutex/shortener/ImageController.java
Normal file
|
@ -0,0 +1,57 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@CrossOrigin(origins = "http://localhost:63342")
|
||||
@RequestMapping("/api/images")
|
||||
|
||||
public class ImageController {
|
||||
|
||||
@Autowired
|
||||
private ImageService imageService;
|
||||
|
||||
|
||||
|
||||
@PostMapping("/upload")
|
||||
public ResponseEntity<Object> uploadImage(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
|
||||
String imageUrl = imageService.uploadImage(file);
|
||||
|
||||
if (imageUrl != null) {
|
||||
// Get the server name and port from the request
|
||||
String serverName = request.getServerName();
|
||||
int serverPort = request.getServerPort();
|
||||
String proto = request.getScheme();
|
||||
|
||||
// Construct the full image URL
|
||||
String fullImageUrl = proto + "://" + serverName + ":" + serverPort + imageUrl;
|
||||
|
||||
// Create a response object
|
||||
Map<String, String> response = new HashMap<>();
|
||||
response.put("location", fullImageUrl);
|
||||
|
||||
// Return the response as JSON
|
||||
return ResponseEntity.ok(response);
|
||||
} else {
|
||||
// Return an error response
|
||||
Map<String, String> errorResponse = new HashMap<>();
|
||||
errorResponse.put("error", "Failed to upload image.");
|
||||
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{imageId}")
|
||||
public ResponseEntity<byte[]> getImageById(@PathVariable Long imageId) {
|
||||
ResponseEntity<byte[]> image = imageService.getImageContent(imageId);
|
||||
return image;
|
||||
}
|
||||
}
|
52
src/main/java/com/bitmutex/shortener/ImageEntity.java
Normal file
52
src/main/java/com/bitmutex/shortener/ImageEntity.java
Normal file
|
@ -0,0 +1,52 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "image_entity")
|
||||
public class ImageEntity {
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Id
|
||||
//@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String filename;
|
||||
|
||||
private String contentType;
|
||||
|
||||
private byte[] content;
|
||||
|
||||
// Constructors, getters, and setters...
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public byte[] getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(byte[] content) {
|
||||
this.content = content;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ImageRepository extends JpaRepository<ImageEntity, Long> {
|
||||
}
|
10
src/main/java/com/bitmutex/shortener/ImageService.java
Normal file
10
src/main/java/com/bitmutex/shortener/ImageService.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
// ImageService interface
|
||||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface ImageService {
|
||||
String uploadImage(MultipartFile file);
|
||||
ResponseEntity<byte[]> getImageContent(Long imageId);
|
||||
}
|
97
src/main/java/com/bitmutex/shortener/ImageServiceImpl.java
Normal file
97
src/main/java/com/bitmutex/shortener/ImageServiceImpl.java
Normal file
|
@ -0,0 +1,97 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
public class ImageServiceImpl implements ImageService {
|
||||
|
||||
@Autowired
|
||||
private ImageRepository imageRepository;
|
||||
|
||||
@Override
|
||||
public String uploadImage(MultipartFile file) {
|
||||
try {
|
||||
// Generate a random image ID and check if it's unique
|
||||
long randomImageId;
|
||||
do {
|
||||
randomImageId = generateUniqueImageId();
|
||||
} while (imageRepository.existsById(randomImageId));
|
||||
|
||||
// Create a new ImageEntity
|
||||
ImageEntity imageEntity = new ImageEntity();
|
||||
imageEntity.setId(randomImageId);
|
||||
imageEntity.setFilename(file.getOriginalFilename());
|
||||
imageEntity.setContentType(file.getContentType());
|
||||
imageEntity.setContent(file.getBytes());
|
||||
|
||||
// Save the ImageEntity to the database
|
||||
ImageEntity savedImage = imageRepository.save(imageEntity);
|
||||
|
||||
|
||||
// Return the URL for accessing the image content
|
||||
return "/api/images/" + savedImage.getId();
|
||||
} catch (IOException e) {
|
||||
// Handle the exception (e.g., log or throw a custom exception)
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<byte[]> getImageContent(Long imageId) {
|
||||
Optional<ImageEntity> optionalImage = imageRepository.findById(imageId);
|
||||
|
||||
if (optionalImage.isPresent()) {
|
||||
ImageEntity image = optionalImage.get();
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.IMAGE_PNG); // Set the appropriate content type
|
||||
|
||||
// Return the image data along with headers
|
||||
return new ResponseEntity<>(image.getContent(), headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
private static long generateUniqueImageId() {
|
||||
try {
|
||||
// Use SHA-256 hash for generating a unique ID
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] bytes = new byte[16];
|
||||
random.nextBytes(bytes);
|
||||
byte[] hashBytes = digest.digest(bytes);
|
||||
|
||||
// Convert the byte array to a numeric value (long)
|
||||
long numericValue = 0;
|
||||
for (byte hashByte : hashBytes) {
|
||||
numericValue = numericValue * 256 + (hashByte & 0xFF);
|
||||
}
|
||||
|
||||
// Ensure a fixed length (12 digits)
|
||||
long twelveDigitId = Math.abs(numericValue) % 1_000_000_000_000L;
|
||||
|
||||
// If needed, pad with leading zeros to ensure 12 digits
|
||||
return twelveDigitId;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Handle the exception (e.g., log or throw a custom exception)
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
24
src/main/java/com/bitmutex/shortener/LoginController.java
Normal file
24
src/main/java/com/bitmutex/shortener/LoginController.java
Normal file
|
@ -0,0 +1,24 @@
|
|||
package com.bitmutex.shortener;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/")
|
||||
public class LoginController {
|
||||
|
||||
@GetMapping("/login")
|
||||
String login() {
|
||||
return "login";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/register")
|
||||
public String register() {
|
||||
return "register"; // Thymeleaf template name without the extension
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public class MaxBioPagesLimitExceededException extends RuntimeException {
|
||||
|
||||
public MaxBioPagesLimitExceededException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MaxBioPagesLimitExceededException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public class MaxShortUrlLimitExceededException extends RuntimeException {
|
||||
|
||||
public MaxShortUrlLimitExceededException() {
|
||||
super("Maximum short URL limit exceeded for the user.");
|
||||
}
|
||||
|
||||
public MaxShortUrlLimitExceededException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MaxShortUrlLimitExceededException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||
import org.springframework.boot.actuate.info.InfoEndpoint;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Controller
|
||||
public class MonitoringController {
|
||||
private final HealthEndpoint healthEndpoint;
|
||||
private final InfoEndpoint infoEndpoint;
|
||||
private final UserService userService;
|
||||
|
||||
|
||||
public MonitoringController(HealthEndpoint healthEndpoint, InfoEndpoint infoEndpoint, UserService userService) {
|
||||
this.healthEndpoint = healthEndpoint;
|
||||
this.infoEndpoint = infoEndpoint;
|
||||
this.userService = userService;
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("/monitoring")
|
||||
public String monitoringPanel(Authentication authentication,Model model) {
|
||||
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
String username = authentication.getName();
|
||||
UserDetailsDto user = userService.getUserDetailsByUsername(username);
|
||||
UserEntity userElevated = userService.findByEmail(user.getEmail());
|
||||
model.addAttribute("user", user);
|
||||
model.addAttribute("username", user.getUsername());
|
||||
model.addAttribute("userelevated", userElevated);
|
||||
}
|
||||
|
||||
// Retrieve Actuator data
|
||||
String healthData = healthEndpoint.health().toString();
|
||||
String infoData = infoEndpoint.info().toString();
|
||||
|
||||
System.out.println( healthData);
|
||||
System.out.println( infoData);
|
||||
|
||||
// Add data to the model
|
||||
|
||||
model.addAttribute("healthData", healthData);
|
||||
model.addAttribute("infoData", infoData);
|
||||
|
||||
// Return the monitoring panel template
|
||||
return "monitoring";
|
||||
}
|
||||
}
|
55
src/main/java/com/bitmutex/shortener/OtpEntity.java
Normal file
55
src/main/java/com/bitmutex/shortener/OtpEntity.java
Normal file
|
@ -0,0 +1,55 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
public class OtpEntity {
|
||||
|
||||
@Id
|
||||
private String username;
|
||||
private String phoneNumber;
|
||||
private String otp;
|
||||
|
||||
private String email;
|
||||
|
||||
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
// Getters and Setters
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public void setPhoneNumber(String phoneNumber) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
|
||||
public String getOtp() {
|
||||
return otp;
|
||||
}
|
||||
|
||||
public void setOtp(String otp) {
|
||||
this.otp = otp;
|
||||
}
|
||||
}
|
10
src/main/java/com/bitmutex/shortener/OtpRepository.java
Normal file
10
src/main/java/com/bitmutex/shortener/OtpRepository.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface OtpRepository extends JpaRepository<OtpEntity, String> {
|
||||
// Custom query to find an OTP entry by username and phone number
|
||||
OtpEntity findByUsernameAndPhoneNumber(String username, String phoneNumber);
|
||||
OtpEntity findByEmail(String email);
|
||||
OtpEntity removeByEmail(String email);
|
||||
}
|
80
src/main/java/com/bitmutex/shortener/OtpService.java
Normal file
80
src/main/java/com/bitmutex/shortener/OtpService.java
Normal file
|
@ -0,0 +1,80 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class OtpService {
|
||||
|
||||
@Autowired
|
||||
private OtpRepository otpRepository;
|
||||
|
||||
|
||||
|
||||
// Generate and store OTP for a given username and phone number
|
||||
public String generateAndStoreOtp(String username, String phoneNumber) {
|
||||
// Generate a simple 4-digit OTP (you may use a more secure method)
|
||||
String generatedOtp = String.valueOf((int) (Math.random() * 9000) + 1000);
|
||||
|
||||
// Store the OTP in the database
|
||||
OtpEntity otpEntity = new OtpEntity();
|
||||
otpEntity.setUsername(username);
|
||||
otpEntity.setPhoneNumber(phoneNumber);
|
||||
otpEntity.setOtp(generatedOtp);
|
||||
|
||||
otpRepository.save(otpEntity);
|
||||
|
||||
return generatedOtp;
|
||||
}
|
||||
|
||||
public String generateAndStoreOtp(String email) {
|
||||
// Generate a simple 4-digit OTP (you may use a more secure method)
|
||||
String generatedOtp = String.valueOf((int) (Math.random() * 9000) + 1000);
|
||||
|
||||
// Store the OTP in the database
|
||||
OtpEntity otpEntity = new OtpEntity();
|
||||
otpEntity.setUsername("NA");
|
||||
otpEntity.setPhoneNumber("NA");
|
||||
otpEntity.setEmail(email);
|
||||
otpEntity.setOtp(generatedOtp);
|
||||
|
||||
otpRepository.save(otpEntity);
|
||||
|
||||
return generatedOtp;
|
||||
}
|
||||
|
||||
|
||||
// Retrieve stored OTP for a given username and phone number
|
||||
public Optional<String> getOtp(String username, String phoneNumber) {
|
||||
OtpEntity otpEntity = otpRepository.findByUsernameAndPhoneNumber(username, phoneNumber);
|
||||
return Optional.ofNullable(otpEntity).map(OtpEntity::getOtp);
|
||||
}
|
||||
|
||||
public Optional<String> getOtp(String email) {
|
||||
OtpEntity otpEntity = otpRepository.findByEmail(email);
|
||||
return Optional.ofNullable(otpEntity).map(OtpEntity::getOtp);
|
||||
}
|
||||
|
||||
// Remove the stored OTP for a given username and phone number
|
||||
public void removeOtp(String username, String phoneNumber) {
|
||||
otpRepository.deleteById(username + phoneNumber);
|
||||
}
|
||||
|
||||
|
||||
// Remove the stored OTP for a given email
|
||||
public void removeOtpByEmail(String email) {
|
||||
OtpEntity otpEntity = otpRepository.findByEmail(email);
|
||||
|
||||
if (otpEntity != null) {
|
||||
otpRepository.delete(otpEntity);
|
||||
} else {
|
||||
// Handle the case when no entry is found for the given email
|
||||
throw new NoSuchElementException("No entry found for email: " + email);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
5
src/main/java/com/bitmutex/shortener/PaymentService.java
Normal file
5
src/main/java/com/bitmutex/shortener/PaymentService.java
Normal file
|
@ -0,0 +1,5 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public interface PaymentService {
|
||||
boolean verifyPayment(RazorpayPaymentDetails paymentDetails);
|
||||
}
|
167
src/main/java/com/bitmutex/shortener/ProfileController.java
Normal file
167
src/main/java/com/bitmutex/shortener/ProfileController.java
Normal file
|
@ -0,0 +1,167 @@
|
|||
package com.bitmutex.shortener;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
|
||||
@RequestMapping("/")
|
||||
@Controller
|
||||
public class ProfileController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private OtpService otpService;
|
||||
|
||||
@Autowired
|
||||
private SmsService smsService;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@GetMapping("/profile")
|
||||
public String viewProfile(@RequestParam String username, Model model) {
|
||||
|
||||
// Fetch the user details from the database
|
||||
try {
|
||||
UserDetailsDto user = userService.getUserDetailsByUsername(username);
|
||||
UserEntity userElevated = userService.findByEmail(user.getEmail());
|
||||
|
||||
|
||||
// Add user details to the model
|
||||
model.addAttribute("user", user);
|
||||
model.addAttribute("userelevated", userElevated);
|
||||
}catch (UsernameNotFoundException ex){
|
||||
return "error";
|
||||
}
|
||||
return "profile";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@PostMapping("/profile/update-phone-number")
|
||||
public String updatePhoneNumber(@RequestParam String username,
|
||||
@RequestParam String newPhoneNumber,
|
||||
Model model,
|
||||
HttpServletRequest request) {
|
||||
|
||||
try {
|
||||
// Generate and send OTP to the user's new phone number
|
||||
String generatedOtp = otpService.generateAndStoreOtp(username, newPhoneNumber);
|
||||
|
||||
HttpSession session = request.getSession();
|
||||
session.setAttribute("otp", generatedOtp);
|
||||
|
||||
// Send the OTP to the user (you need to implement this part)
|
||||
// var client = HttpClient.newHttpClient();
|
||||
// var apiKey = "Vucf1nCa4ed-AMNGv6CnsycfQT28yLUA8NEvY7IZ87-Piv855UBcjfo29Zb8XPZt";
|
||||
|
||||
|
||||
|
||||
var messageVariable = "Your OTP is :"+ generatedOtp; // Define your variable with the desired content
|
||||
smsService.sendSms(newPhoneNumber,messageVariable);
|
||||
|
||||
|
||||
// Store the generated OTP and the associated phone number in a secure way
|
||||
//otpService.storeOtp(username, newPhoneNumber, generatedOtp);
|
||||
|
||||
// Add necessary details to the model for displaying the OTP verification form
|
||||
model.addAttribute("username", username);
|
||||
model.addAttribute("newPhoneNumber", newPhoneNumber);
|
||||
model.addAttribute("generatedOtp", generatedOtp);
|
||||
|
||||
// Redirect to the OTP verification page
|
||||
return "redirect:/profile/verify-otp?username="+username+"&newPhoneNumber="+newPhoneNumber;
|
||||
|
||||
} catch (Exception ex) {
|
||||
model.addAttribute("error", "Failed to initiate phone number update.");
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/profile/verify-otp")
|
||||
public String verifyOtpAndUpdatePhoneNumber(@RequestParam String username,
|
||||
@RequestParam String newPhoneNumber,
|
||||
@RequestParam String enteredOtp,
|
||||
RedirectAttributes redirectAttributes,
|
||||
Model model) {
|
||||
|
||||
try {
|
||||
//Get User Details
|
||||
UserEntity user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||
model.addAttribute("username", user.getUsername());
|
||||
// Retrieve the stored OTP associated with the username and new phone number
|
||||
// String storedOtp = otpService.getOtp(username, newPhoneNumber);
|
||||
// Verify the entered OTP
|
||||
if (otpService.getOtp(username, newPhoneNumber).orElse("").equals(enteredOtp)) {
|
||||
// OTP is correct, update the phone number in the database
|
||||
user.setPhoneNumber(newPhoneNumber);
|
||||
userRepository.save(user);
|
||||
|
||||
// Remove the stored OTP after successful verification
|
||||
otpService.removeOtp(username, newPhoneNumber);
|
||||
|
||||
// Add success message to the model
|
||||
model.addAttribute("success", "Phone number updated successfully.");
|
||||
return"redirect:/profile?username="+username;
|
||||
} else {
|
||||
// Incorrect OTP, add an error message to the flash attributes
|
||||
redirectAttributes.addFlashAttribute("error", "Incorrect OTP. Please try again.");
|
||||
|
||||
return "redirect:/profile/verify-otp?username=" + username + "&newPhoneNumber=" + newPhoneNumber;
|
||||
}
|
||||
|
||||
// Verify the entered OTP
|
||||
/* if (enteredOtp.equals("1234")) {
|
||||
|
||||
//Save Phone Number to DB
|
||||
user.setPhoneNumber(newPhoneNumber);
|
||||
userRepository.save(user);
|
||||
http://localhost:8080/profile?username=amit
|
||||
// Add success message to the model
|
||||
model.addAttribute("success", "Phone number updated successfully.");
|
||||
return"redirect:/profile?username="+username;
|
||||
} else {
|
||||
// Incorrect OTP, add an error message to the model
|
||||
model.addAttribute("error", "Incorrect OTP. Please try again.");
|
||||
return "redirect:/profile/verify-otp?error";
|
||||
} */
|
||||
|
||||
} catch (Exception ex) {
|
||||
model.addAttribute("error", "Failed to verify OTP and update phone number.");
|
||||
}
|
||||
|
||||
return "verify-otp";
|
||||
}
|
||||
|
||||
@GetMapping("/profile/verify-otp")
|
||||
public String showOtpForm(@RequestParam String username,
|
||||
@RequestParam String newPhoneNumber,
|
||||
Model model) {
|
||||
|
||||
model.addAttribute("username", username);
|
||||
model.addAttribute("newPhoneNumber", newPhoneNumber);
|
||||
return "verify-otp";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/user/public/check")
|
||||
public class PublicUserController {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@GetMapping("/username")
|
||||
public ResponseEntity<Map<String, Boolean>> checkUsernameExists(@RequestParam String username) {
|
||||
boolean usernameExists = userRepository.existsByUsername(username);
|
||||
|
||||
Map<String, Boolean> response = new HashMap<>();
|
||||
response.put("exists", usernameExists);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/email")
|
||||
public ResponseEntity<Map<String, Boolean>> checkEmailExists(@RequestParam String email) {
|
||||
boolean emailExists = userRepository.existsByEmail(email);
|
||||
|
||||
Map<String, Boolean> response = new HashMap<>();
|
||||
response.put("exists", emailExists);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
17
src/main/java/com/bitmutex/shortener/RateLimitConfig.java
Normal file
17
src/main/java/com/bitmutex/shortener/RateLimitConfig.java
Normal file
|
@ -0,0 +1,17 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class RateLimitConfig {
|
||||
|
||||
@Value("${requests.per.second}")
|
||||
private double permitsPerSecond;
|
||||
@Bean
|
||||
public RateLimiter rateLimiter() {
|
||||
return RateLimiter.create(permitsPerSecond); // Adjust the rate (permits per second) using the configured value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class RazorpayPaymentDetails {
|
||||
|
||||
private Long userId;
|
||||
private BigDecimal amount;
|
||||
private String transactionId;
|
||||
|
||||
// Constructors, getters, setters, etc.
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public class RegistrationException extends RuntimeException {
|
||||
|
||||
public RegistrationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public RegistrationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public class RegistrationRequest {
|
||||
private String username;
|
||||
private String password;
|
||||
private String email;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
|
||||
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
// getters and setters
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public class RegistrationResponse {
|
||||
private String message;
|
||||
|
||||
// getters and setters
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/reset-password")
|
||||
public class ResetPasswordController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService; // Assuming you have a UserService
|
||||
public static PasswordEncoder passwordEncoder;
|
||||
|
||||
public ResetPasswordController(PasswordEncoder passwordEncoder) {
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String showResetPasswordForm(@RequestParam("token") String token, Model model) {
|
||||
UserEntity user = userService.findByResetToken(token);
|
||||
|
||||
if (user != null && user.getResetTokenExpiryDateTime().isAfter(LocalDateTime.now())) {
|
||||
// Validate the reset token and its expiration
|
||||
model.addAttribute("token", token);
|
||||
return "reset-password";
|
||||
} else {
|
||||
// Token is invalid or expired
|
||||
return "redirect:/forgot-password?error";
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String processResetPassword(@RequestParam("token") String token, @RequestParam("password") String newPassword, Model model) {
|
||||
UserEntity user = userService.findByResetToken(token);
|
||||
|
||||
if (user != null && user.getResetTokenExpiryDateTime().isAfter(LocalDateTime.now())) {
|
||||
|
||||
if (passwordEncoder.matches(newPassword, user.getPassword())) {
|
||||
// New password is the same as the old password
|
||||
return "redirect:/forgot-password?same";
|
||||
}
|
||||
|
||||
// Logic to validate the reset token and set the new password
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
user.setResetToken(null);
|
||||
user.setResetTokenExpiryDateTime(null);
|
||||
userService.save(user);
|
||||
|
||||
// For simplicity, let's assume the password is reset successfully
|
||||
return "redirect:/login?passwordResetSuccess";
|
||||
} else {
|
||||
// Token is invalid or expired
|
||||
return "redirect:/forgot-password?error";
|
||||
}
|
||||
}
|
||||
}
|
91
src/main/java/com/bitmutex/shortener/SecurityConfig.java
Normal file
91
src/main/java/com/bitmutex/shortener/SecurityConfig.java
Normal file
|
@ -0,0 +1,91 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Autowired
|
||||
private UserDetailsServiceImpl userDetailsService;
|
||||
|
||||
|
||||
@Bean
|
||||
public static PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> securityConfigurerAdapter() {
|
||||
return new SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>() {
|
||||
@Override
|
||||
public void configure(HttpSecurity http) {
|
||||
http.authenticationProvider(authenticationProvider());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
|
||||
authenticationProvider.setUserDetailsService(userDetailsService);
|
||||
authenticationProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authenticationProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf->csrf.disable())
|
||||
.authorizeRequests(authorizeRequests -> authorizeRequests
|
||||
.requestMatchers("/").permitAll()
|
||||
.requestMatchers("/api/url/**").authenticated()
|
||||
.requestMatchers("/register").permitAll()
|
||||
.requestMatchers("/perform_login").permitAll()
|
||||
.requestMatchers("/submitContactForm").permitAll()
|
||||
.requestMatchers("/forgot-password", "/reset-password").permitAll()
|
||||
.requestMatchers("/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
|
||||
|
||||
.formLogin(formLogin -> formLogin
|
||||
.loginPage("/login").permitAll() // Specifies the custom login page URL
|
||||
//.loginProcessingUrl("/perform_login") // Specifies the custom login processing URL
|
||||
//.defaultSuccessUrl("/", true)
|
||||
//.failureUrl("/login?error=true") // Redirect to login page with error parameter
|
||||
)
|
||||
.oauth2Login(oauth2Login -> oauth2Login
|
||||
.loginPage("/login") // Customize login page if needed
|
||||
.defaultSuccessUrl("/") // Customize default success URL after OAuth2 login
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessUrl("/")
|
||||
// .logoutSuccessUrl("/login?logout=true")
|
||||
.invalidateHttpSession(true)
|
||||
)
|
||||
.headers(headers->headers.frameOptions(frame->frame.disable())) //enable iframe from anywhere
|
||||
.httpBasic(Customizer.withDefaults());
|
||||
//.authenticationProvider(authenticationProvider());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
45
src/main/java/com/bitmutex/shortener/SmsService.java
Normal file
45
src/main/java/com/bitmutex/shortener/SmsService.java
Normal file
|
@ -0,0 +1,45 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
@Service
|
||||
public class SmsService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SmsService.class);
|
||||
|
||||
public void sendSms(String toNumber, String message) {
|
||||
try {
|
||||
// Send the SMS to the user (you need to implement this part)
|
||||
var client = HttpClient.newHttpClient();
|
||||
var apiKey = "Vucf1nCa4ed-AMNGv6CnsycfQT28yLUA8NEvY7IZ87-Piv855UBcjfo29Zb8XPZt";
|
||||
|
||||
String payload = "{\n" +
|
||||
" \"content\": \"" + message + "\",\n" + // Use concatenation for variables
|
||||
" \"from\": \"+9038556097\",\n" +
|
||||
" \"to\": \"" + toNumber + "\"\n" +
|
||||
"}";
|
||||
|
||||
var requestBuild = HttpRequest.newBuilder()
|
||||
.uri(URI.create("https://api.httpsms.com/v1/messages/send"))
|
||||
.header("accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-api-key", apiKey)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(payload))
|
||||
.build();
|
||||
|
||||
var response = client.send(requestBuild, HttpResponse.BodyHandlers.ofString());
|
||||
System.out.println(response.body());
|
||||
logger.info("SMS sent successfully to {}", toNumber);
|
||||
logger.debug("SMS API response: {}", response.body());
|
||||
} catch (Exception e) {
|
||||
// Log and handle SMS sending failure
|
||||
logger.error("Failed to send SMS to {}: {}", toNumber, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
8
src/main/java/com/bitmutex/shortener/SourceType.java
Normal file
8
src/main/java/com/bitmutex/shortener/SourceType.java
Normal file
|
@ -0,0 +1,8 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public enum SourceType {
|
||||
WEBSITE,
|
||||
FRIEND,
|
||||
SOCIAL_MEDIA,
|
||||
OTHER
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/status")
|
||||
public class StatusCheckController {
|
||||
|
||||
|
||||
private final StatusCheckService statusCheckService;
|
||||
|
||||
@Autowired
|
||||
public StatusCheckController(StatusCheckService statusCheckService) {
|
||||
this.statusCheckService = statusCheckService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Integer>> getServerStatus() {
|
||||
Map<String, Integer> status = statusCheckService.getServerStatus();
|
||||
return ResponseEntity.ok(status);
|
||||
}
|
||||
}
|
66
src/main/java/com/bitmutex/shortener/StatusCheckService.java
Normal file
66
src/main/java/com/bitmutex/shortener/StatusCheckService.java
Normal file
|
@ -0,0 +1,66 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
@Slf4j
|
||||
@Service
|
||||
public class StatusCheckService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Value("${database.port}") // Inject the server port from application properties
|
||||
private String dbPort;
|
||||
|
||||
@Value("${database.name}") // Inject the server port from application properties
|
||||
private String dbName;
|
||||
|
||||
@Autowired
|
||||
public StatusCheckService(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getServerStatus() {
|
||||
Map<String, Integer> status = new HashMap<>();
|
||||
// Add logic to check the server status
|
||||
boolean isServerHealthy = checkServerHealth();
|
||||
if(isServerHealthy)
|
||||
log.info("Server healthy");
|
||||
status.put("server_status", isServerHealthy ? 1 : 0);
|
||||
return status;
|
||||
}
|
||||
|
||||
private boolean checkServerHealth() {
|
||||
try {
|
||||
String sql = "SELECT SCHEMA_NAME\n" +
|
||||
" FROM INFORMATION_SCHEMA.SCHEMATA\n" +
|
||||
" WHERE SCHEMA_NAME = '" + dbName + "'";
|
||||
// Execute a simple query to check the database connection
|
||||
jdbcTemplate.execute(sql);
|
||||
log.info("DB connection healthy");
|
||||
// Check if the required table exists
|
||||
boolean isTableExists = doesTableExist("analytics");
|
||||
log.info("DB Tables Found");
|
||||
// You can add additional checks here based on your requirements
|
||||
|
||||
return isTableExists;
|
||||
} catch (Exception e) {
|
||||
return false; // Database connection failed or table does not exist
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doesTableExist(String tableName) {
|
||||
try {
|
||||
// Execute a query to check if the table exists
|
||||
jdbcTemplate.execute("SELECT 1 FROM " + tableName + " WHERE 1 = 0");
|
||||
return true; // Table exists
|
||||
} catch (Exception e) {
|
||||
return false; // Table does not exist
|
||||
}
|
||||
}
|
||||
}
|
61
src/main/java/com/bitmutex/shortener/Subscription.java
Normal file
61
src/main/java/com/bitmutex/shortener/Subscription.java
Normal file
|
@ -0,0 +1,61 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
//CONTAINS MAPPING WITH USER ID AND PLAN ID
|
||||
@Entity
|
||||
@Table(name = "subscription")
|
||||
public class Subscription {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "subscription_id")
|
||||
private Long id;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public SubscriptionPlan getPlan() {
|
||||
return plan;
|
||||
}
|
||||
|
||||
public void setPlan(SubscriptionPlan plan) {
|
||||
this.plan = plan;
|
||||
}
|
||||
|
||||
public String getRazorpaySubscriptionId() {
|
||||
return razorpaySubscriptionId;
|
||||
}
|
||||
|
||||
public void setRazorpaySubscriptionId(String razorpaySubscriptionId) {
|
||||
this.razorpaySubscriptionId = razorpaySubscriptionId;
|
||||
}
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
private UserEntity user;
|
||||
|
||||
public UserEntity getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(UserEntity user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "plan_id")
|
||||
private SubscriptionPlan plan;
|
||||
|
||||
@Column(name = "razorpay_subscription_id", nullable = true)
|
||||
private String razorpaySubscriptionId;
|
||||
|
||||
// Getters and setters, constructors, etc.
|
||||
|
||||
// Constructors, getters, setters, etc.
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import com.bitmutex.shortener.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/subscription")
|
||||
public class SubscriptionController {
|
||||
|
||||
@Autowired
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping("/change")
|
||||
public ResponseEntity<?> changeSubscription(@RequestParam String username, @RequestParam String newPlanName) {
|
||||
try {
|
||||
UserEntity userEntity = userService.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||
|
||||
if (userEntity == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body("User not found with username: " + username);
|
||||
}
|
||||
|
||||
subscriptionService.changeSubscription(userEntity, newPlanName);
|
||||
|
||||
return ResponseEntity.ok("Subscription changed successfully");
|
||||
} catch (Exception ex) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Error changing subscription: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/details/{username}")
|
||||
public ResponseEntity<Object> getSubscriptionDetails(@PathVariable String username) {
|
||||
try {
|
||||
// Find the user by username
|
||||
UserEntity userEntity = userService.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||
|
||||
if (userEntity == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// Get the subscription details for the user
|
||||
Map<String, Object> subscriptionDetails = subscriptionService.getCurrentSubscriptionDetails(userEntity);
|
||||
|
||||
return ResponseEntity.ok(subscriptionDetails);
|
||||
} catch (Exception e) {
|
||||
// Handle exceptions and return an appropriate response
|
||||
return ResponseEntity.status(500).body("Internal Server Error");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public class SubscriptionNotFoundException extends RuntimeException {
|
||||
public SubscriptionNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package com.bitmutex.shortener;
|
||||
import java.math.BigDecimal;
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "subscription_payment")
|
||||
public class SubscriptionPayment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "payment_id")
|
||||
private Long id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "subscription_id")
|
||||
private Subscription subscription;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Subscription getSubscription() {
|
||||
return subscription;
|
||||
}
|
||||
|
||||
public void setSubscription(Subscription subscription) {
|
||||
this.subscription = subscription;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(BigDecimal amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public String getPaymentStatus() {
|
||||
return paymentStatus;
|
||||
}
|
||||
|
||||
public void setPaymentStatus(String paymentStatus) {
|
||||
this.paymentStatus = paymentStatus;
|
||||
}
|
||||
|
||||
|
||||
@Column(name = "amount", nullable = false)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "payment_status", nullable = false)
|
||||
private String paymentStatus;
|
||||
@CreationTimestamp
|
||||
@Column(name = "timestamp", nullable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
|
||||
private Timestamp timestamp;
|
||||
|
||||
// Getters and setters, constructors, etc.
|
||||
|
||||
// Constructors, getters, setters, etc.
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface SubscriptionPaymentRepository extends JpaRepository<SubscriptionPayment, Long> {
|
||||
|
||||
// You can add custom query methods here if needed
|
||||
}
|
85
src/main/java/com/bitmutex/shortener/SubscriptionPlan.java
Normal file
85
src/main/java/com/bitmutex/shortener/SubscriptionPlan.java
Normal file
|
@ -0,0 +1,85 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
// CONTAINS DETAILS OF EACH SUBSCRIPTION PLAN
|
||||
@Entity
|
||||
@Table(name = "subscription_plan")
|
||||
public class SubscriptionPlan {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "plan_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "plan_name", unique = true, nullable = false)
|
||||
private String planName;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getPlanName() {
|
||||
return planName;
|
||||
}
|
||||
|
||||
public void setPlanName(String planName) {
|
||||
this.planName = planName;
|
||||
}
|
||||
|
||||
public String getRazorpayPlanId() {
|
||||
return razorpayPlanId;
|
||||
}
|
||||
|
||||
public void setRazorpayPlanId(String razorpayPlanId) {
|
||||
this.razorpayPlanId = razorpayPlanId;
|
||||
}
|
||||
|
||||
public int getMaxShortUrls() {
|
||||
return maxShortUrls;
|
||||
}
|
||||
|
||||
public void setMaxShortUrls(int maxShortUrls) {
|
||||
this.maxShortUrls = maxShortUrls;
|
||||
}
|
||||
|
||||
public int getMaxBioPages() {
|
||||
return maxBioPages;
|
||||
}
|
||||
|
||||
public void setMaxBioPages(int maxBioPages) {
|
||||
this.maxBioPages = maxBioPages;
|
||||
}
|
||||
|
||||
@Column(name = "razorpay_plan_id", nullable = false)
|
||||
private String razorpayPlanId;
|
||||
|
||||
@Column(name = "max_short_urls", nullable = false)
|
||||
private int maxShortUrls;
|
||||
|
||||
@Column(name = "max_bio_pages", nullable = false)
|
||||
private int maxBioPages;
|
||||
|
||||
|
||||
// Check if the plan is free
|
||||
public boolean isFree() {
|
||||
return "Free".equalsIgnoreCase(planName);
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(BigDecimal price) {
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
private BigDecimal price; // New field for the plan price
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public class SubscriptionPlanNotFoundException extends RuntimeException {
|
||||
public SubscriptionPlanNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SubscriptionPlanRepository extends JpaRepository<SubscriptionPlan, Long> {
|
||||
Optional<SubscriptionPlan> findByPlanName(String planName);
|
||||
|
||||
Optional<SubscriptionPlan> findSubscriptionPlanByPlanName(String planName);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface SubscriptionRepository extends JpaRepository<Subscription, Long> {
|
||||
// You can add custom queries or methods related to subscriptions here
|
||||
// Custom query to find a subscription by user
|
||||
Subscription findByUser(UserEntity user);
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SubscriptionService {
|
||||
|
||||
private final SubscriptionRepository subscriptionRepository;
|
||||
private final SubscriptionPlanRepository subscriptionPlanRepository;
|
||||
|
||||
private final SubscriptionPaymentRepository subscriptionPaymentRepository;
|
||||
|
||||
@Autowired
|
||||
public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionPlanRepository subscriptionPlanRepository,SubscriptionPaymentRepository subscriptionPaymentRepository) {
|
||||
this.subscriptionRepository = subscriptionRepository;
|
||||
this.subscriptionPlanRepository = subscriptionPlanRepository;
|
||||
this.subscriptionPaymentRepository = subscriptionPaymentRepository;
|
||||
}
|
||||
|
||||
public void createSubscription(UserEntity userEntity, String planName) {
|
||||
// Retrieve the subscription plan
|
||||
SubscriptionPlan subscriptionPlan = subscriptionPlanRepository.findSubscriptionPlanByPlanName(planName)
|
||||
.orElseThrow(() -> new SubscriptionPlanNotFoundException("Subscription plan not found: " + planName));
|
||||
|
||||
try{
|
||||
// Create a new subscription
|
||||
Subscription subscription = new Subscription();
|
||||
subscription.setUser(userEntity);
|
||||
subscription.setPlan(subscriptionPlan);
|
||||
|
||||
// Save the subscription to the database
|
||||
subscriptionRepository.save(subscription);
|
||||
|
||||
log.info("SUCCESS: Plan with name:"+subscriptionPlan.getPlanName()+"set for user with username:"+userEntity.getUsername());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.error(ex.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void changeSubscription(UserEntity userEntity, String newPlanName) {
|
||||
// Retrieve the current subscription of the user
|
||||
Subscription currentSubscription = subscriptionRepository.findByUser(userEntity);
|
||||
|
||||
// Retrieve the new subscription plan
|
||||
SubscriptionPlan newSubscriptionPlan = subscriptionPlanRepository.findByPlanName(newPlanName)
|
||||
.orElseThrow(() -> new SubscriptionPlanNotFoundException("Subscription plan not found: " + newPlanName));
|
||||
|
||||
// Check if the new plan is the same as the current plan
|
||||
if (currentSubscription.getPlan().equals(newSubscriptionPlan)) {
|
||||
// No need to change the subscription, as the user is already on the desired plan
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Save the new subscription and update the user's subscription plan
|
||||
currentSubscription.setPlan(newSubscriptionPlan);
|
||||
// currentSubscription.setRazorpaySubscriptionId();
|
||||
subscriptionRepository.save(currentSubscription);
|
||||
|
||||
// Record the payment in the existing subscription_payment table
|
||||
SubscriptionPayment subscriptionPayment = new SubscriptionPayment();
|
||||
subscriptionPayment.setSubscription(currentSubscription);
|
||||
subscriptionPayment.setAmount(newSubscriptionPlan.getPrice());
|
||||
subscriptionPayment.setPaymentStatus("SUCCESS"); // Set payment status based on actual payment verification
|
||||
subscriptionPaymentRepository.save(subscriptionPayment);
|
||||
|
||||
}
|
||||
|
||||
public Map<String, Object> getCurrentSubscriptionDetails(UserEntity userEntity) {
|
||||
// Retrieve the current subscription of the user
|
||||
Subscription currentSubscription = subscriptionRepository.findByUser(userEntity);
|
||||
|
||||
// Retrieve additional details such as subscription plan name, max short URL, max bio pages, etc.
|
||||
SubscriptionPlan currentSubscriptionPlan = currentSubscription.getPlan();
|
||||
|
||||
// Create a map to store the subscription details
|
||||
Map<String, Object> subscriptionDetails = new HashMap<>();
|
||||
subscriptionDetails.put("planName", currentSubscriptionPlan.getPlanName());
|
||||
subscriptionDetails.put("maxShortUrl", currentSubscriptionPlan.getMaxShortUrls());
|
||||
subscriptionDetails.put("maxBioPages", currentSubscriptionPlan.getMaxBioPages());
|
||||
// Add more fields as needed
|
||||
|
||||
return subscriptionDetails;
|
||||
}
|
||||
}
|
74
src/main/java/com/bitmutex/shortener/UpdateUserRequest.java
Normal file
74
src/main/java/com/bitmutex/shortener/UpdateUserRequest.java
Normal file
|
@ -0,0 +1,74 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
// UpdateUserRequest.java
|
||||
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class UpdateUserRequest {
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public byte[] getProfilePicture() {
|
||||
return profilePicture;
|
||||
}
|
||||
|
||||
public void setProfilePicture(byte[] profilePicture) {
|
||||
this.profilePicture = profilePicture;
|
||||
}
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public void setPhoneNumber(String phoneNumber) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
private String email;
|
||||
private String firstName;
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
private String lastName;
|
||||
private byte[] profilePicture;
|
||||
private String phoneNumber;
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class UrlDetailsController {
|
||||
UrlShortenerService urlShortenerService;
|
||||
|
||||
@GetMapping("/details")
|
||||
public String getUrlDetails(@RequestParam String shortUrl, Model model) {
|
||||
|
||||
// Add the details to the model
|
||||
model.addAttribute("shortUrl", shortUrl);
|
||||
|
||||
// Return the name of the details HTML file (without extension)
|
||||
return "details";
|
||||
}
|
||||
}
|
123
src/main/java/com/bitmutex/shortener/UrlDetailsDTO.java
Normal file
123
src/main/java/com/bitmutex/shortener/UrlDetailsDTO.java
Normal file
|
@ -0,0 +1,123 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
|
||||
public class UrlDetailsDTO {
|
||||
private Long id;
|
||||
private String originalUrl;
|
||||
private String shortUrl;
|
||||
private String linkType;
|
||||
private long hits;
|
||||
private Long uniqueHits;
|
||||
private Date timestamp;
|
||||
private String qrCode;
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
private Long userId;
|
||||
|
||||
public void setHits(long hits) {
|
||||
this.hits = hits;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Date timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getQrCode() {
|
||||
return qrCode;
|
||||
}
|
||||
|
||||
public void setQrCode(String qrCode) {
|
||||
this.qrCode = qrCode;
|
||||
}
|
||||
|
||||
public String getBioContent() {
|
||||
return bioContent;
|
||||
}
|
||||
|
||||
public void setBioContent(String bioContent) {
|
||||
this.bioContent = bioContent;
|
||||
}
|
||||
|
||||
private String bioContent;
|
||||
|
||||
// Existing constructors, getters, and setters
|
||||
|
||||
// Constructor with parameters
|
||||
public UrlDetailsDTO(Long id, Long userId, String originalUrl, String shortUrl, String linkType, long hits, Long uniqueHits, Date timestamp, String qrCode, String bioContent) {
|
||||
this.id = id;
|
||||
this.userId=userId;
|
||||
this.originalUrl = originalUrl;
|
||||
this.shortUrl = shortUrl;
|
||||
this.linkType = linkType;
|
||||
this.hits = hits;
|
||||
this.uniqueHits = uniqueHits;
|
||||
this.timestamp = timestamp;
|
||||
this.qrCode = qrCode;
|
||||
this.bioContent = bioContent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getOriginalUrl() {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
public void setOriginalUrl(String originalUrl) {
|
||||
this.originalUrl = originalUrl;
|
||||
}
|
||||
|
||||
public String getShortUrl() {
|
||||
return shortUrl;
|
||||
}
|
||||
|
||||
public void setShortUrl(String shortUrl) {
|
||||
this.shortUrl = shortUrl;
|
||||
}
|
||||
|
||||
public String getLinkType() {
|
||||
return linkType;
|
||||
}
|
||||
|
||||
public void setLinkType(String linkType) {
|
||||
this.linkType = linkType;
|
||||
}
|
||||
|
||||
public Long getHits() {
|
||||
return hits;
|
||||
}
|
||||
|
||||
public void setHits(Long hits) {
|
||||
this.hits = hits;
|
||||
}
|
||||
|
||||
public Long getUniqueHits() {
|
||||
return uniqueHits;
|
||||
}
|
||||
|
||||
public void setUniqueHits(Long uniqueHits) {
|
||||
this.uniqueHits = uniqueHits;
|
||||
}
|
||||
|
||||
// Constructors, getters, and setters
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
public class UrlRedirectionController {
|
||||
|
||||
@Autowired
|
||||
private UrlShortenerRepository urlShortenerRepository;
|
||||
|
||||
@Autowired
|
||||
private AnalyticsRepository analyticsRepository;
|
||||
|
||||
@Autowired
|
||||
private UrlShortenerService urlShortenerService;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@PostMapping("{shortUrl}")
|
||||
public String submitPassword(@PathVariable String shortUrl,
|
||||
@RequestParam String password,
|
||||
HttpSession session,
|
||||
Model model) {
|
||||
UrlShortener urlShortener = urlShortenerRepository.findByShortUrl(shortUrl);
|
||||
// Validate the password (you may want to check it against a stored hash)
|
||||
|
||||
//boolean isValidPassword = urlShortener.getPassword().equals(password);
|
||||
// boolean isValidPassword = passwordEncoder.matches(urlShortener.getPassword(),password);
|
||||
boolean isValidPassword = urlShortenerService.validatePassword(shortUrl, password);
|
||||
|
||||
|
||||
if (isValidPassword) {
|
||||
// Store an indicator in the session that the user is authenticated
|
||||
session.setAttribute("authenticated", true);
|
||||
|
||||
// Redirect to the original URL
|
||||
return "redirect:/" + shortUrl;
|
||||
} else {
|
||||
// Password is incorrect, redirect back to the password prompt with an error flag
|
||||
model.addAttribute("error", true);
|
||||
return "password_prompt";
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{shortUrl}")
|
||||
public Object redirect(@PathVariable String shortUrl,
|
||||
HttpServletResponse response,
|
||||
HttpServletRequest request,
|
||||
@RequestParam(required = false) String password,
|
||||
@RequestParam(required = false) boolean error, // New parameter to indicate password error
|
||||
Model model,
|
||||
HttpSession session) throws IOException {
|
||||
UrlShortener urlShortener = urlShortenerRepository.findByShortUrl(shortUrl);
|
||||
if (urlShortener != null) {
|
||||
long startTime = System.nanoTime();
|
||||
|
||||
// Capture analytics data
|
||||
Analytics analytics = new Analytics();
|
||||
analytics.setUrlShortener(urlShortener);
|
||||
analytics.setDeviceIp(getOriginalIp(request));
|
||||
// analytics.setUid(generateUid()); // You need to implement generateUid() method
|
||||
analytics.setTimezone(request.getHeader("Time-Zone")); // Assuming you get timezone from header
|
||||
analytics.setAcceptTypes(request.getHeader("Accept"));
|
||||
analytics.setAcceptLanguage(request.getHeader("Accept-Language"));
|
||||
analytics.setDeviceType(request.getHeader("User-Agent")); // Assuming you get device type from User-Agent header
|
||||
request.getRemoteUser();
|
||||
analytics.setTimestamp(String.valueOf(new Date())); // Assuming you want to capture the timestamp
|
||||
|
||||
// Update visit count or hits
|
||||
long hits = getVisitCount(shortUrl);
|
||||
urlShortener.setHits(hits);
|
||||
|
||||
// Update unique hits count
|
||||
long uniqueHitsCount = getUniqueHitsCount(shortUrl);
|
||||
urlShortener.setUniqueHits(uniqueHitsCount);
|
||||
|
||||
urlShortenerRepository.save(urlShortener);
|
||||
|
||||
if (urlShortener.getLinkStatus() == 0) {
|
||||
System.out.println(urlShortener.getLinkStatus());
|
||||
log.info("Disable link blocked with short URL: " + shortUrl);
|
||||
// Handle case where link is disabled
|
||||
return "disabled";
|
||||
}
|
||||
// Check if the user is authenticated (session contains the 'authenticated' attribute)
|
||||
boolean isAuthenticated = session.getAttribute("authenticated") != null;
|
||||
if (!isAuthenticated) {
|
||||
|
||||
if (urlShortener.getPassword() != null) {
|
||||
// If the URL is password-protected and no password is provided, show the password form
|
||||
if (password == null) {
|
||||
model.addAttribute("shortUrl", shortUrl);
|
||||
return "password_prompt";
|
||||
}
|
||||
|
||||
// Check if the provided password is correct
|
||||
if (!urlShortener.getPassword().equals(password)) {
|
||||
model.addAttribute("shortUrl", shortUrl);
|
||||
model.addAttribute("error", true); // Set error attribute
|
||||
return "password_prompt"; // Stay on the password prompt page with error message
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("bio".equals(urlShortener.getLinkType())) {
|
||||
// For bio pages, render custom HTML content
|
||||
renderBioPage(response, urlShortener);
|
||||
log.info("Successful creation of Bio-Page with shortcode: " + shortUrl );
|
||||
// Save analytics data to the database
|
||||
analyticsRepository.save(analytics);
|
||||
}
|
||||
|
||||
else {
|
||||
// Redirect to the original URL
|
||||
String originalUrl = urlShortener.getOriginalUrl();
|
||||
response.sendRedirect(originalUrl);
|
||||
|
||||
// Calculate and set redirection time
|
||||
long endTime = System.nanoTime();
|
||||
long redirectionTimeNano = endTime - startTime;
|
||||
double redirectionTimeMillis = redirectionTimeNano / 1000000f; // ns to ms 10^-6
|
||||
analytics.SetRedirectionTime(redirectionTimeMillis);
|
||||
|
||||
// Save analytics data to the database
|
||||
analyticsRepository.save(analytics);
|
||||
|
||||
log.info("Successful Redirection of short URL: " + shortUrl + "to original URL: " + originalUrl + " in " + redirectionTimeMillis + " nanoseconds");
|
||||
}
|
||||
} else {
|
||||
// Handle not found scenario (e.g., show an error page)
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Short URL not found");
|
||||
}
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("No Allowed Methods Found");
|
||||
}
|
||||
private String getOriginalIp(HttpServletRequest request) {
|
||||
String xForwardedForHeader = request.getHeader("X-Forwarded-For");
|
||||
if (xForwardedForHeader != null && !xForwardedForHeader.isEmpty()) {
|
||||
// The client's original IP address is the first IP in the list
|
||||
String originalIp = xForwardedForHeader.split(",")[0].trim();
|
||||
|
||||
// Try to convert IPv6 to IPv4 format
|
||||
try {
|
||||
InetAddress inetAddress = InetAddress.getByName(originalIp);
|
||||
if (inetAddress instanceof java.net.Inet6Address) {
|
||||
// Convert IPv6 to IPv4 format if necessary
|
||||
originalIp = inetAddress.getHostAddress();
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
// Handle the exception, e.g., log or ignore
|
||||
}
|
||||
|
||||
return originalIp;
|
||||
}
|
||||
// If X-Forwarded-For is not present, fall back to the remote address
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
public long getVisitCount(String shortUrl) {
|
||||
UrlShortener urlShortener = urlShortenerRepository.findByShortUrl(shortUrl);
|
||||
|
||||
if (urlShortener != null) {
|
||||
return analyticsRepository.countByUrlShortenerId(urlShortener.getId());
|
||||
} else {
|
||||
throw new UrlShortenerException("Short URL not found: " + shortUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public long getUniqueHitsCount(String shortUrl) {
|
||||
UrlShortener urlShortener = urlShortenerRepository.findByShortUrl(shortUrl);
|
||||
|
||||
if (urlShortener != null) {
|
||||
return urlShortenerRepository.countUniqueHitsByUrlShortenerId(urlShortener.getId());
|
||||
} else {
|
||||
throw new UrlShortenerException("Short URL not found: " + shortUrl);
|
||||
}
|
||||
}
|
||||
private void renderBioPage(HttpServletResponse response, UrlShortener urlShortener) throws IOException {
|
||||
// Set content type to HTML
|
||||
response.setContentType("text/html;charset=UTF-8");
|
||||
|
||||
// Use custom HTML content for bio-page, or default content if not provided
|
||||
String bioContent = urlShortener.getBioContent();
|
||||
if (bioContent == null || bioContent.isEmpty()) {
|
||||
bioContent = getDefaultBioContent();
|
||||
}
|
||||
|
||||
// Write the HTML content to the response
|
||||
try (PrintWriter writer = response.getWriter()) {
|
||||
writer.write(bioContent);
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private String getDefaultBioContent() {
|
||||
// Provide default HTML content for bio-pages
|
||||
return "<html><head><title>Bio Page</title></head><body><h1>Welcome to the Bio Page!</h1></body></html>";
|
||||
}
|
||||
|
||||
}
|
266
src/main/java/com/bitmutex/shortener/UrlShortener.java
Normal file
266
src/main/java/com/bitmutex/shortener/UrlShortener.java
Normal file
|
@ -0,0 +1,266 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.EncodeHintType;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.client.j2se.MatrixToImageConfig;
|
||||
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "url_shortener")
|
||||
public class UrlShortener {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String originalUrl;
|
||||
private String shortUrl;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@OneToMany(mappedBy = "urlShortener", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
@JsonIgnore
|
||||
private List<Analytics> analyticsList;
|
||||
|
||||
@Column(name = "link_type")
|
||||
private String linkType;
|
||||
|
||||
public int getLinkStatus() {
|
||||
return linkStatus;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Column(name = "password")
|
||||
|
||||
private String password;
|
||||
|
||||
public void setLinkStatus(int linkStatus) {
|
||||
this.linkStatus = linkStatus;
|
||||
}
|
||||
|
||||
@Column(name = "link_status")
|
||||
private int linkStatus;
|
||||
|
||||
// Constructors
|
||||
|
||||
public UrlShortener() {
|
||||
// Default constructor required by JPA
|
||||
}
|
||||
|
||||
public UrlShortener(String originalUrl, String shortUrl) {
|
||||
this.originalUrl = originalUrl;
|
||||
this.shortUrl = shortUrl;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getOriginalUrl() {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
public void setOriginalUrl(String originalUrl) {
|
||||
this.originalUrl = originalUrl;
|
||||
}
|
||||
|
||||
public String getShortUrl() {
|
||||
return shortUrl;
|
||||
}
|
||||
|
||||
public void setShortUrl(String shortUrl) {
|
||||
this.shortUrl = shortUrl;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getLinkType() {
|
||||
return linkType;
|
||||
}
|
||||
|
||||
public void setLinkType(String linkType) {
|
||||
this.linkType = linkType;
|
||||
}
|
||||
|
||||
public List<Analytics> getAnalyticsList() {
|
||||
return analyticsList;
|
||||
}
|
||||
|
||||
public void setAnalyticsList(List<Analytics> analyticsList) {
|
||||
this.analyticsList = analyticsList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//HIT COUNT
|
||||
@Column(name = "hits")
|
||||
private long hits;
|
||||
public long getHits() {
|
||||
return hits;
|
||||
}
|
||||
public void setHits(long hits) {
|
||||
this.hits = hits;
|
||||
}
|
||||
|
||||
//UNIQUE HIT COUNT
|
||||
@Column(name = "unique_hits")
|
||||
private Long uniqueHits;
|
||||
public Long getUniqueHits() {
|
||||
return uniqueHits;
|
||||
}
|
||||
public void setUniqueHits(Long uniqueHits) {
|
||||
this.uniqueHits = uniqueHits;
|
||||
}
|
||||
|
||||
public String getQrCode() {
|
||||
return qrCode;
|
||||
}
|
||||
|
||||
public void setQrCode(String qrCode) {
|
||||
this.qrCode = qrCode;
|
||||
}
|
||||
|
||||
//QR CODE
|
||||
@Column(name = "qr_code", length = 2048) // Adjust the length based on your QR code image size
|
||||
private String qrCode;
|
||||
public void generateQrCode(String serverBaseUrl, String logoPath, String fgColor, String bgColor) {
|
||||
// Construct the full URL including the server base URL and shortcode
|
||||
String fullUrl = serverBaseUrl + "/" + this.getShortUrl();
|
||||
|
||||
// Generate the QR code and store it as a Base64-encoded string
|
||||
this.qrCode = generateQrCodeBase64(fullUrl, fgColor,bgColor, logoPath);
|
||||
}
|
||||
|
||||
private String generateQrCodeBase64(String content, String foregroundColorHex, String backgroundColorHex, String logoPath) {
|
||||
try {
|
||||
int width = 300; // Set the width and height of the QR code image
|
||||
int height = 300;
|
||||
|
||||
// Convert hex strings to Color objects or use default colors
|
||||
Color foregroundColor = getColorFromHex(foregroundColorHex, Color.RED);
|
||||
Color backgroundColor = getColorFromHex(backgroundColorHex, Color.WHITE);
|
||||
|
||||
Map<EncodeHintType, Object> hints = new HashMap<>();
|
||||
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
|
||||
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
|
||||
|
||||
QRCodeWriter writer = new QRCodeWriter();
|
||||
BitMatrix bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints);
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
MatrixToImageConfig config = new MatrixToImageConfig(foregroundColor.getRGB(), backgroundColor.getRGB());
|
||||
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream, config);
|
||||
|
||||
BufferedImage qrImage = ImageIO.read(new ByteArrayInputStream(outputStream.toByteArray()));
|
||||
|
||||
if (logoPath != null && !logoPath.isEmpty()) {
|
||||
// Add logo to the center of the QR code
|
||||
addLogoToQrCode(qrImage, logoPath);
|
||||
}
|
||||
|
||||
// Convert the modified image to a Base64-encoded string
|
||||
ByteArrayOutputStream modifiedStream = new ByteArrayOutputStream();
|
||||
ImageIO.write(qrImage, "PNG", modifiedStream);
|
||||
byte[] imageBytes = modifiedStream.toByteArray();
|
||||
|
||||
return Base64.getEncoder().encodeToString(imageBytes);
|
||||
} catch (Exception e) {
|
||||
// Handle exceptions appropriately
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private void addLogoToQrCode(BufferedImage qrImage, String logoPath) throws IOException {
|
||||
// Read the logo image
|
||||
BufferedImage logoImage = ImageIO.read(new File(logoPath));
|
||||
|
||||
// Calculate the position to place the logo at the center
|
||||
int logoX = (qrImage.getWidth() - logoImage.getWidth()) / 2;
|
||||
int logoY = (qrImage.getHeight() - logoImage.getHeight()) / 2;
|
||||
|
||||
// Draw the logo on the QR code
|
||||
Graphics2D graphics = qrImage.createGraphics();
|
||||
graphics.drawImage(logoImage, logoX, logoY, null);
|
||||
graphics.dispose();
|
||||
}
|
||||
private Color getColorFromHex(String hexColor, Color defaultColor) {
|
||||
try {
|
||||
return Color.decode(hexColor);
|
||||
} catch (NumberFormatException e) {
|
||||
// If parsing fails, return the default color
|
||||
return defaultColor;
|
||||
}
|
||||
}
|
||||
public String getBioContent() {
|
||||
return bioContent;
|
||||
}
|
||||
|
||||
public void setBioContent(String bioContent) {
|
||||
this.bioContent = bioContent;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Date timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Column(name = "created_at", columnDefinition = "DATE")
|
||||
private Date timestamp;
|
||||
|
||||
|
||||
//BIO PAGE
|
||||
@Column(name = "bio_content", columnDefinition = "TEXT")
|
||||
private String bioContent;
|
||||
|
||||
// toString, hashCode, equals, if necessary
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UrlShortener{" +
|
||||
"id=" + id +
|
||||
", originalUrl='" + originalUrl + '\'' +
|
||||
", shortUrl='" + shortUrl + '\'' +
|
||||
", userId=" + userId +
|
||||
", linkType='" + linkType + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
//@ComponentScan(basePackages = "com.bitmutex.shortener")
|
||||
public class UrlShortenerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(UrlShortenerApplication.class, args);
|
||||
}
|
||||
}
|
375
src/main/java/com/bitmutex/shortener/UrlShortenerController.java
Normal file
375
src/main/java/com/bitmutex/shortener/UrlShortenerController.java
Normal file
|
@ -0,0 +1,375 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/url")
|
||||
public class UrlShortenerController {
|
||||
|
||||
@Value("${server.port}") // Inject the server port from application properties
|
||||
private String serverPort;
|
||||
|
||||
@Value("${server.servlet.context-path:}") // Inject the context path from application properties
|
||||
private String contextPath;
|
||||
|
||||
@Autowired
|
||||
private UrlShortenerService service;
|
||||
|
||||
@Autowired
|
||||
private RateLimiter rateLimiter;
|
||||
|
||||
@Autowired
|
||||
private UrlShortenerRepository urlShortenerRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Value("${cooldown.duration}")
|
||||
private long cooldownSeconds;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
private final ConcurrentHashMap<String, Long> cooldownMap = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
|
||||
@PostMapping("/shorten")
|
||||
public ResponseEntity<String> shortenUrl(@RequestBody String originalUrl, Model model) {
|
||||
try {
|
||||
String clientId = getClientId();
|
||||
|
||||
if (isOnCooldown(clientId)) {
|
||||
model.addAttribute("error", "Rate limit exceeded. Cooling down.");
|
||||
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Rate limit exceeded. Cooling down.");
|
||||
}
|
||||
|
||||
if (rateLimiter.tryAcquire()) {
|
||||
// SERVICE CALL
|
||||
String shortUrl = service.shortenUrl(originalUrl);
|
||||
return ResponseEntity.ok(shortUrl);
|
||||
} else {
|
||||
setCooldown(clientId);
|
||||
model.addAttribute("error", "Rate limit exceeded. Cooldown initiated.");
|
||||
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Rate limit exceeded. Cooldown initiated.");
|
||||
}
|
||||
} catch (UrlShortenerException e) {
|
||||
model.addAttribute("error", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
model.addAttribute("error", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/original")
|
||||
public ResponseEntity<String> getOriginalUrl(@RequestParam String shortUrl) {
|
||||
try {
|
||||
String originalUrl = service.getOriginalUrl(shortUrl);
|
||||
return ResponseEntity.ok(originalUrl);
|
||||
} catch (RuntimeException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred while processing the request.");
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/remove")
|
||||
public ResponseEntity<String> removeShortUrl(@RequestParam String shortUrl) {
|
||||
try {
|
||||
service.removeShortUrl(shortUrl);
|
||||
return ResponseEntity.ok("Short URL removed successfully.");
|
||||
} catch (RuntimeException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred while processing the request.");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/qr/generate")
|
||||
public ResponseEntity<String> generateAndRetrieveQrCode(@RequestParam String shortUrl,@RequestParam String logoPath,@RequestParam String fgColor,@RequestParam String bgColor) {
|
||||
try {
|
||||
String qrCode = service.generateAndRetrieveQrCode(shortUrl,logoPath,fgColor,bgColor);
|
||||
return ResponseEntity.ok(qrCode);
|
||||
} catch (UrlShortenerException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred while processing the request.");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/qr/image/{shortUrl}")
|
||||
public ResponseEntity<byte[]> getQrCode(@PathVariable String shortUrl) {
|
||||
try {
|
||||
UrlShortener urlShortener = urlShortenerRepository.findByShortUrl(shortUrl);
|
||||
|
||||
if (urlShortener != null && urlShortener.getQrCode() != null) {
|
||||
// Decode the Base64-encoded QR code
|
||||
byte[] qrCodeBytes = Base64.getDecoder().decode(urlShortener.getQrCode());
|
||||
|
||||
return ResponseEntity
|
||||
.ok()
|
||||
.contentType(MediaType.IMAGE_PNG)
|
||||
.body(qrCodeBytes);
|
||||
} else {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Handle exceptions appropriately
|
||||
e.printStackTrace();
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/bio/create")
|
||||
public ResponseEntity<String> createBioPage(@RequestBody Map<String, String> requestBody, HttpServletRequest request, Model model) {
|
||||
try {
|
||||
String clientId = getClientId();
|
||||
|
||||
if (isOnCooldown(clientId)) {
|
||||
model.addAttribute("error", "Rate limit exceeded. Cooling down.");
|
||||
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Rate limit exceeded. Cooling down.");
|
||||
}
|
||||
|
||||
if (rateLimiter.tryAcquire()) {
|
||||
String customHtmlContent = requestBody.get("customHtmlContent");
|
||||
UrlShortener bioPage = service.createBioPage(customHtmlContent);
|
||||
|
||||
// Determine the protocol (HTTP or HTTPS) dynamically
|
||||
String protocol = request.isSecure() ? "https" : "http";
|
||||
|
||||
// Get the server name/ip
|
||||
String serverName = request.getServerName();
|
||||
|
||||
// Construct the bio page URL
|
||||
String bioPageUrl = protocol + "://" + serverName + ":" + serverPort + contextPath + "/" + bioPage.getShortUrl();
|
||||
|
||||
return ResponseEntity.ok(bioPageUrl);
|
||||
} else {
|
||||
setCooldown(clientId);
|
||||
model.addAttribute("error", "Rate limit exceeded. Cooldown initiated.");
|
||||
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Rate limit exceeded. Cooldown initiated.");
|
||||
}
|
||||
} catch (UrlShortenerException e) {
|
||||
model.addAttribute("error", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
model.addAttribute("error", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@PutMapping("/bio/edit")
|
||||
public ResponseEntity<String> editBioPageContent(@RequestParam String shortUrl, @RequestBody Map<String, String> requestBody) {
|
||||
try {
|
||||
String customHtmlContent = requestBody.get("customHtmlContent");
|
||||
UrlShortener editedBioPage = service.editBioPageContent(shortUrl, customHtmlContent);
|
||||
return ResponseEntity.ok("Bio page content edited successfully");
|
||||
} catch (UrlShortenerException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("An error occurred while processing the request.");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("bio/content")
|
||||
public String getBioContent(String shortUrl) {
|
||||
// Fetch the bio content from the url_shortener table using the shortUrl
|
||||
UrlShortener urlShortener = service.findByShortUrl(shortUrl);
|
||||
|
||||
if (urlShortener != null) {
|
||||
return urlShortener.getBioContent();
|
||||
} else {
|
||||
return "Bio content not found for the given short URL.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/getUrlsByUsername")
|
||||
public ResponseEntity<List<UrlShortener>> getUrlsByUsername(@RequestParam String username) {
|
||||
try {
|
||||
// Step 2: Extract the username parameter from the request
|
||||
|
||||
// Step 3: Use the username to retrieve the corresponding user_id from the user_entity table
|
||||
Optional<UserEntity> userEntityOptional = userRepository.findByUsername(username);
|
||||
if (userEntityOptional.isPresent()) {
|
||||
UserEntity userEntity = userEntityOptional.get();
|
||||
Long userId = userEntity.getId();
|
||||
|
||||
// Step 4: Use the obtained user_id to fetch the URLs from the url_shortener table
|
||||
List<UrlShortener> urls = service.getUrlsByUserId(userId);
|
||||
|
||||
// Step 5: Return the URLs details as a response
|
||||
return ResponseEntity.ok(urls);
|
||||
} else {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); // User not found
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/details")
|
||||
public ResponseEntity getUrlDetails(@RequestParam String shortUrl, Authentication authentication, Model model) {
|
||||
try {
|
||||
String name = authentication.getName();
|
||||
|
||||
UserDetailsDto user = userService.getUserDetailsByUsername(name);
|
||||
UserEntity userElevated = userService.findByEmail(user.getEmail());
|
||||
|
||||
UrlShortener urlShortener = service.getUrlDetailsByShortUrl(shortUrl);
|
||||
|
||||
model.addAttribute("user",user);
|
||||
model.addAttribute("userElevated",userElevated);
|
||||
|
||||
|
||||
|
||||
if (urlShortener != null) {
|
||||
UrlDetailsDTO urlDetailsDTO = new UrlDetailsDTO(
|
||||
urlShortener.getId(),
|
||||
urlShortener.getUserId(),
|
||||
urlShortener.getOriginalUrl(),
|
||||
urlShortener.getShortUrl(),
|
||||
urlShortener.getLinkType(),
|
||||
urlShortener.getHits(),
|
||||
urlShortener.getUniqueHits(),
|
||||
urlShortener.getTimestamp(),
|
||||
urlShortener.getQrCode(),
|
||||
urlShortener.getBioContent()
|
||||
);
|
||||
return ResponseEntity.status(HttpStatus.OK).body(urlDetailsDTO); // User not found
|
||||
} else {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); // User not found
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/edit")
|
||||
public ResponseEntity<String> editUrl(
|
||||
@RequestParam String oldShortUrl,
|
||||
@RequestParam String newShortUrl) {
|
||||
|
||||
// Check if the new short URL already exists
|
||||
UrlShortener existingUrl = service.findByShortUrl(newShortUrl);
|
||||
if (existingUrl != null) {
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).body("New short URL already exists.");
|
||||
}
|
||||
|
||||
// Proceed with the update if the new short URL is unique
|
||||
UrlShortener urlToEdit = service.findByShortUrl(oldShortUrl);
|
||||
if (urlToEdit != null) {
|
||||
urlToEdit.setShortUrl(newShortUrl);
|
||||
service.saveOrUpdate(urlToEdit);
|
||||
return ResponseEntity.ok("URL updated successfully.");
|
||||
} else {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Old short URL not found.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/status")
|
||||
public ResponseEntity<Integer> getLinkStatus(@RequestParam String shortUrl) {
|
||||
int linkStatus = service.getLinkStatusByShortUrl(shortUrl);
|
||||
return new ResponseEntity<>(linkStatus, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/setstatus")
|
||||
public ResponseEntity<String> setLinkStatus(
|
||||
@RequestParam String shortUrl,
|
||||
@RequestParam int linkStatus) {
|
||||
service.setLinkStatusByShortUrl(shortUrl, linkStatus);
|
||||
return new ResponseEntity<>("Link status set successfully", HttpStatus.OK);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/set-password")
|
||||
public ResponseEntity<String> setPassword(
|
||||
@RequestParam String shortUrl,
|
||||
@RequestParam(required = false) String password
|
||||
) {
|
||||
UrlShortener urlShortener = service.findByShortUrl(shortUrl);
|
||||
|
||||
if (urlShortener == null) {
|
||||
return new ResponseEntity<>("Short URL not found", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// Set the password (or set it to null to remove the password)
|
||||
urlShortener.setPassword(password);
|
||||
|
||||
// Save the updated UrlShortener entity
|
||||
service.savePassword(urlShortener);
|
||||
|
||||
return new ResponseEntity<>("Password set successfully", HttpStatus.OK);
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("get-password")
|
||||
public ResponseEntity<String> getPassword(
|
||||
@RequestParam String shortUrl) {
|
||||
String password = service.getPasswordByShortUrl(shortUrl);
|
||||
return new ResponseEntity<>(password, HttpStatus.OK);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("reset-password")
|
||||
public ResponseEntity<String> setPasswordToNull(@RequestParam String shortUrl) {
|
||||
try {
|
||||
service.setPasswordToNull(shortUrl);
|
||||
return new ResponseEntity<>("Password set to null successfully / Successful Reset", HttpStatus.OK);
|
||||
} catch (Exception e) {
|
||||
return new ResponseEntity<>("Error setting password to null: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOnCooldown(String clientId) {
|
||||
Long cooldownEndTime = cooldownMap.get(clientId);
|
||||
return cooldownEndTime != null && cooldownEndTime > System.currentTimeMillis();
|
||||
}
|
||||
private void setCooldown(String clientId) {
|
||||
// Set the cooldown period from application properties
|
||||
long cooldownDuration = TimeUnit.SECONDS.toMillis(cooldownSeconds);
|
||||
|
||||
// Calculate the cooldown end time
|
||||
long cooldownEndTime = System.currentTimeMillis() + cooldownDuration;
|
||||
|
||||
// Set the cooldown end time in the map
|
||||
cooldownMap.put(clientId, cooldownEndTime);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected String getClientId() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
String clientIp = request.getRemoteAddr();
|
||||
return "ip_" + clientIp; // Prefixing with "ip_" for clarity
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public class UrlShortenerException extends RuntimeException {
|
||||
|
||||
public UrlShortenerException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UrlShortenerException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@Transactional
|
||||
@Repository
|
||||
public interface UrlShortenerRepository extends JpaRepository<UrlShortener, Long> {
|
||||
|
||||
List<UrlShortener> findByUserId(Long userId);
|
||||
|
||||
|
||||
UrlShortener findByShortUrl(String shortUrl);
|
||||
|
||||
// Add a method to find UrlShortener by ID
|
||||
UrlShortener findById(long id);
|
||||
|
||||
// Add a method to find analytics by UrlShortener
|
||||
// List<Analytics> findAnalyticsListByUrlShortener_Id(Long urlShortenerId);
|
||||
@Query("SELECT COUNT(DISTINCT a.deviceIp) FROM Analytics a WHERE a.urlShortener.id = :urlShortenerId")
|
||||
Long countUniqueHitsByUrlShortenerId(@Param("urlShortenerId") Long urlShortenerId);
|
||||
|
||||
@Query("SELECT u.qrCode FROM UrlShortener u WHERE u.shortUrl = :shortUrl")
|
||||
String findQrCodeByShortUrl(@Param("shortUrl") String shortUrl);
|
||||
|
||||
|
||||
UrlShortener findByShortUrlAndLinkType(String shortUrl, String linkType);
|
||||
|
||||
// Updated methods
|
||||
@Query("SELECT u.linkStatus FROM UrlShortener u WHERE u.shortUrl = :shortUrl")
|
||||
int getLinkStatusByShortUrl(@Param("shortUrl") String shortUrl);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE UrlShortener u SET u.linkStatus = :linkStatus WHERE u.shortUrl = :shortUrl")
|
||||
void setLinkStatusByShortUrl(@Param("shortUrl") String shortUrl, @Param("linkStatus") int linkStatus);
|
||||
|
||||
|
||||
// Count short URLs by user ID
|
||||
@Query("SELECT COUNT(u) FROM UrlShortener u WHERE u.userId = :userId AND u.linkType = 'short'")
|
||||
int countShortUrlsByUserId(@Param("userId") Long userId);
|
||||
|
||||
// Count bio pages by user ID
|
||||
@Query("SELECT COUNT(u) FROM UrlShortener u WHERE u.userId = :userId AND u.linkType = 'bio'")
|
||||
int countBioPagesByUserId(@Param("userId") Long userId);
|
||||
|
||||
}
|
424
src/main/java/com/bitmutex/shortener/UrlShortenerService.java
Normal file
424
src/main/java/com/bitmutex/shortener/UrlShortenerService.java
Normal file
|
@ -0,0 +1,424 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UrlShortenerService {
|
||||
|
||||
@Autowired
|
||||
private UrlShortenerRepository repository;
|
||||
|
||||
@Autowired
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private HttpServletRequest request; // Inject the HttpServletRequest
|
||||
|
||||
@Value("${server.port}") // Inject the server port from application properties
|
||||
private String serverPort;
|
||||
|
||||
@Value("${server.servlet.context-path:}") // Inject the context path from application properties
|
||||
private String contextPath;
|
||||
|
||||
@Value("${server.base.url}")
|
||||
private String serverBaseUrl;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
public String shortenUrl(String jsonInput) {
|
||||
try {
|
||||
String originalUrl = extractOriginalUrl(jsonInput);
|
||||
|
||||
// Validate the original URL
|
||||
validateOriginalUrl(originalUrl);
|
||||
|
||||
// Ensure the URL has a valid scheme (http:// or https://)
|
||||
if (!originalUrl.startsWith("http://") && !originalUrl.startsWith("https://")) {
|
||||
originalUrl = "https://" + originalUrl;
|
||||
}
|
||||
//Check if URL returns 200OK
|
||||
if(!isValidHttpResponse(originalUrl))
|
||||
throw new UrlShortenerException("Shortening Error: Url does not give 200OK, we won't shorten this");
|
||||
|
||||
// Generate a new shortcode
|
||||
String shortCode = generateShortCode(originalUrl);
|
||||
|
||||
// Get the user ID (you need to implement a method to fetch the current user ID)
|
||||
Long userId = getCurrentUserId();
|
||||
|
||||
// Set linkType to "short" (as per your requirement)
|
||||
String linkType = "short";
|
||||
|
||||
|
||||
|
||||
// Get the user entity (you need to implement a method to fetch the current user entity)
|
||||
UserEntity userEntity = userService.findById(userId).get();
|
||||
|
||||
// Check if the user has exceeded the maximum short URL limit
|
||||
int maxShortUrlLimit = Integer.parseInt(subscriptionService.getCurrentSubscriptionDetails(userEntity).get("maxShortUrl").toString());
|
||||
int currentShortUrls = repository.countShortUrlsByUserId(userEntity.getId());
|
||||
|
||||
if (currentShortUrls >= maxShortUrlLimit) {
|
||||
throw new MaxShortUrlLimitExceededException("Maximum short URL limit exceeded for the user.");
|
||||
}
|
||||
|
||||
|
||||
// Save the original, short URL, user ID, and link type to the database
|
||||
UrlShortener urlShortener = new UrlShortener();
|
||||
urlShortener.setOriginalUrl(originalUrl);
|
||||
urlShortener.setShortUrl(shortCode);
|
||||
urlShortener.setUserId(userId);
|
||||
urlShortener.setLinkType(linkType);
|
||||
urlShortener.setLinkStatus(1);
|
||||
repository.save(urlShortener);
|
||||
log.info("Saved Record to DB");
|
||||
|
||||
// Get the server name/ip
|
||||
String serverName = request.getServerName();
|
||||
|
||||
// Construct the complete URL using HTTPS (if applicable)
|
||||
String protocol = request.isSecure() ? "https" : "http";
|
||||
String completeUrl = protocol + "://" + serverName + ":" + serverPort + contextPath + "/" + shortCode;
|
||||
|
||||
|
||||
// log.info("Successfully Shortened URL : " + originalUrl + " -> " + completeUrl + " [SHORTCODE: " + shortCode + "]");
|
||||
log.info("Successfully Shortened URL:From {} to-> {} [SHORTCODE: {}]", originalUrl, completeUrl, shortCode);
|
||||
|
||||
|
||||
// Return the complete URL
|
||||
return completeUrl;
|
||||
}catch (MaxShortUrlLimitExceededException e) {
|
||||
// Handle the exception as needed
|
||||
log.error("Max short URL limit exceeded for the user", e);
|
||||
throw e;
|
||||
}
|
||||
catch(UrlShortenerException e)
|
||||
{
|
||||
log.error("Shortening Error: Validation Phase");
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Handle other exceptions
|
||||
log.error("Shortening Error", e);
|
||||
throw new UrlShortenerException("Shortening Error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateShortCode(String originalUrl) {
|
||||
try {
|
||||
// Introduce randomness with UUID
|
||||
String randomPart = UUID.randomUUID().toString().replaceAll("-", "");
|
||||
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hashBytes = digest.digest((originalUrl + randomPart).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Convert the hash to a URL-friendly string
|
||||
String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(hashBytes);
|
||||
|
||||
// Extract only alphabet characters from the encoded hash
|
||||
String alphabetChars = encoded.replaceAll("[^a-zA-Z]", "");
|
||||
|
||||
log.info("Shortcode Generation Successful!");
|
||||
|
||||
// Take the first 8 characters as the shortcode
|
||||
return alphabetChars.substring(0, Math.min(alphabetChars.length(), 8));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("Error generating short code", e);
|
||||
throw new UrlShortenerException("Error generating short code", e);
|
||||
}
|
||||
}
|
||||
private String extractOriginalUrl(String jsonString) {
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
JsonNode jsonNode = objectMapper.readTree(jsonString);
|
||||
String originalUrl = jsonNode.get("originalUrl").asText();
|
||||
|
||||
// Validate the format of the original URL
|
||||
// validateOriginalUrl(originalUrl);
|
||||
log.info("Original URL Extracted from request");
|
||||
return originalUrl;
|
||||
} catch (Exception e) {
|
||||
log.error("Error extracting originalUrl from JSON , BAD JSON Format", e);
|
||||
throw new UrlShortenerException("Error extracting originalUrl from JSON , BAD JSON Format", e);
|
||||
}
|
||||
}
|
||||
public String getOriginalUrl(String shortUrl) {
|
||||
UrlShortener urlShortener = repository.findByShortUrl(shortUrl);
|
||||
|
||||
// Check if the short URL exists in the database
|
||||
if (urlShortener != null) {
|
||||
return urlShortener.getOriginalUrl();
|
||||
} else {
|
||||
// Return null or handle the case when short URL is not found
|
||||
log.error("Short URL not found: " + shortUrl);
|
||||
throw new UrlShortenerException("Short URL not found: " + shortUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeShortUrl(String shortUrl) {
|
||||
UrlShortener urlShortener = repository.findByShortUrl(shortUrl);
|
||||
|
||||
// Check if the short URL exists in the database
|
||||
if (urlShortener != null) {
|
||||
repository.delete(urlShortener);
|
||||
log.info("SHORTURL:"+shortUrl+"Deleted Successfully!");
|
||||
} else {
|
||||
// Handle the case when short URL is not found
|
||||
log.error("Short URL not found: " + shortUrl);
|
||||
throw new UrlShortenerException("Short URL not found: " + shortUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateOriginalUrl(String originalUrl) {
|
||||
if (StringUtils.isEmpty(originalUrl)) {
|
||||
log.error("Original URL cannot be empty or null");
|
||||
throw new UrlShortenerException("Original URL cannot be empty or null");
|
||||
}
|
||||
|
||||
// Regular expression for a simple URL format check
|
||||
String urlRegex = "^(https?://)?([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})(/[a-zA-Z0-9-._?%&=]*)?$";
|
||||
|
||||
try {
|
||||
// Check if the URL has a scheme, if not, assume it is HTTP
|
||||
if (!originalUrl.contains("://")) {
|
||||
originalUrl = "http://" + originalUrl;
|
||||
}
|
||||
|
||||
// Basic URL format check using regex
|
||||
if (!Pattern.matches(urlRegex, originalUrl)) {
|
||||
throw new UrlShortenerException("Invalid URL format: " + originalUrl);
|
||||
}
|
||||
|
||||
UriComponentsBuilder.fromHttpUrl(originalUrl).build().toUri();
|
||||
log.info("URL validation successful for original URL: {}", originalUrl);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Invalid URL format: {}", originalUrl, e);
|
||||
throw new UrlShortenerException("Invalid URL format: " + originalUrl);
|
||||
} catch (Exception e) {
|
||||
log.error("Error validating URL: {}", originalUrl, e);
|
||||
throw new UrlShortenerException("Error validating URL: " + originalUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*private void validateOriginalUrl(String originalUrl) {
|
||||
if (StringUtils.isEmpty(originalUrl)) {
|
||||
log.error("Original URL cannot be empty or null");
|
||||
throw new UrlShortenerException("Original URL cannot be empty or null");
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(originalUrl).toURI(); // Validate URL format
|
||||
log.info("URL Validation Succecssful for original URL:"+originalUrl);
|
||||
} catch (MalformedURLException | IllegalArgumentException | URISyntaxException e) {
|
||||
log.error("Invalid original URL format: " + originalUrl, e);
|
||||
throw new UrlShortenerException("Invalid original URL format: " + originalUrl, e);
|
||||
}
|
||||
}*/
|
||||
|
||||
private Long getCurrentUserId() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if(authentication != null)
|
||||
{
|
||||
Object principal = authentication.getPrincipal();
|
||||
|
||||
if (principal instanceof CustomUserDetails) {
|
||||
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
|
||||
return userDetails.getUserId();
|
||||
}
|
||||
else if (principal instanceof CustomOAuth2User) {
|
||||
UserDetailsDto user = userService.getUserDetailsByUsername(authentication.getName());
|
||||
return user.getUserId();
|
||||
}
|
||||
else
|
||||
return null;
|
||||
}
|
||||
// Return a default value or throw an exception based on your requirements
|
||||
return null;
|
||||
}
|
||||
|
||||
public String generateAndRetrieveQrCode(String shortUrl, String logoPath, String fgColor, String bgColor) {
|
||||
UrlShortener urlShortener = repository.findByShortUrl(shortUrl);
|
||||
|
||||
if (urlShortener != null) {
|
||||
// Generate and set the QR code
|
||||
urlShortener.generateQrCode(serverBaseUrl,logoPath,fgColor,bgColor );
|
||||
|
||||
// Save the updated entity
|
||||
repository.save(urlShortener);
|
||||
|
||||
// Return the QR code
|
||||
return urlShortener.getQrCode();
|
||||
} else {
|
||||
throw new UrlShortenerException("Short URL not found: " + shortUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public UrlShortener createBioPage(String customHtmlContent) {
|
||||
try {
|
||||
Long userId = getCurrentUserId();
|
||||
|
||||
// Get the user entity (you need to implement a method to fetch the current user entity)
|
||||
UserEntity userEntity = userService.findById(userId).get();
|
||||
|
||||
// Check if the user has exceeded the bio pages limit
|
||||
int currentBioPagesCount = repository.countBioPagesByUserId(userId);
|
||||
int maxBioPagesLimit = Integer.parseInt(subscriptionService.getCurrentSubscriptionDetails(userEntity).get("maxBioPages").toString());
|
||||
|
||||
if (currentBioPagesCount >= maxBioPagesLimit) {
|
||||
throw new MaxBioPagesLimitExceededException("Exceeded the maximum allowed number of bio pages.");
|
||||
}
|
||||
|
||||
// Create a new bio page
|
||||
UrlShortener urlShortener = new UrlShortener("N/A", generateShortCode("bio"));
|
||||
urlShortener.setUserId(userId);
|
||||
urlShortener.setLinkType("bio");
|
||||
urlShortener.setLinkStatus(1);
|
||||
urlShortener.setBioContent(customHtmlContent);
|
||||
|
||||
// Save the bio page to the database
|
||||
return repository.save(urlShortener);
|
||||
}catch (MaxBioPagesLimitExceededException e) {
|
||||
log.error("Exceeded the maximum allowed number of bio pages.", e);
|
||||
throw e;
|
||||
}catch (Exception e) {
|
||||
log.error("Error creating bio page", e);
|
||||
throw new UrlShortenerException("Error creating bio page", e);
|
||||
}
|
||||
}
|
||||
|
||||
public UrlShortener editBioPageContent(String shortUrl, String newHtmlContent) {
|
||||
UrlShortener bioPage = repository.findByShortUrlAndLinkType(shortUrl, "bio");
|
||||
|
||||
if (bioPage != null) {
|
||||
bioPage.setBioContent(newHtmlContent);
|
||||
return repository.save(bioPage);
|
||||
} else {
|
||||
throw new UrlShortenerException("Bio page not found for the given short URL: " + shortUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public List<UrlShortener> getUrlsByUserId(Long userId) {
|
||||
try {
|
||||
// Assume that UrlDetails is an entity representing the details of a URL
|
||||
List<UrlShortener> urlDetailsList = repository.findByUserId(userId);
|
||||
|
||||
// You might need to convert entities to a DTO or customize the response based on your needs
|
||||
// For simplicity, let's assume UrlDetails is the entity itself
|
||||
|
||||
return urlDetailsList;
|
||||
} catch (Exception e) {
|
||||
// Handle exceptions or log them as needed
|
||||
throw new UrlShortenerException("Error retrieving URLs by user ID", e);
|
||||
}
|
||||
}
|
||||
|
||||
public UrlShortener getUrlDetailsByShortUrl(String shortUrl) {
|
||||
return repository.findByShortUrl(shortUrl);
|
||||
}
|
||||
|
||||
public UrlShortener findByShortUrl(String shortUrl) {
|
||||
// Implement this method to fetch UrlShortener entity by shortUrl
|
||||
return repository.findByShortUrl(shortUrl);
|
||||
}
|
||||
|
||||
public void saveOrUpdate(UrlShortener urlShortener) {
|
||||
// Your implementation might vary based on the technology you are using (e.g., Spring Data JPA, Hibernate)
|
||||
|
||||
// Check if the entity already has an ID (existing entity) or not (new entity)
|
||||
if (urlShortener.getId() == null) {
|
||||
// This is a new entity, save it
|
||||
repository.save(urlShortener);
|
||||
} else {
|
||||
// This is an existing entity, update it
|
||||
repository.saveAndFlush(urlShortener);
|
||||
}
|
||||
}
|
||||
|
||||
public void savePassword(UrlShortener urlShortener) {
|
||||
// Hash the password before saving
|
||||
String hashedPassword = passwordEncoder.encode(urlShortener.getPassword());
|
||||
urlShortener.setPassword(hashedPassword);
|
||||
|
||||
// Save the UrlShortener entity
|
||||
repository.save(urlShortener);
|
||||
}
|
||||
|
||||
public int getLinkStatusByShortUrl(String shortUrl) {
|
||||
return repository.getLinkStatusByShortUrl(shortUrl);
|
||||
}
|
||||
|
||||
public void setLinkStatusByShortUrl(String shortUrl, int linkStatus) {
|
||||
repository.setLinkStatusByShortUrl(shortUrl, linkStatus);
|
||||
}
|
||||
public boolean validatePassword(String shortUrl, String password) {
|
||||
UrlShortener urlShortener = repository.findByShortUrl(shortUrl);
|
||||
return passwordEncoder.matches(password, urlShortener.getPassword());
|
||||
}
|
||||
|
||||
public void setPasswordByShortUrl(String shortUrl, String password) {
|
||||
UrlShortener urlShortener = repository.findByShortUrl(shortUrl);
|
||||
urlShortener.setPassword(passwordEncoder.encode(password));
|
||||
repository.save(urlShortener);
|
||||
}
|
||||
|
||||
public void setPasswordToNull(String shortUrl) {
|
||||
UrlShortener urlShortener = repository.findByShortUrl(shortUrl);
|
||||
if (urlShortener != null) {
|
||||
urlShortener.setPassword(null);
|
||||
repository.save(urlShortener);
|
||||
} else {
|
||||
throw new EntityNotFoundException("UrlShortener not found for shortUrl: " + shortUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String getPasswordByShortUrl(String shortUrl) {
|
||||
UrlShortener urlShortener = repository.findByShortUrl(shortUrl);
|
||||
return urlShortener.getPassword();
|
||||
}
|
||||
|
||||
|
||||
private boolean isValidHttpResponse(String originalUrl) {
|
||||
try {
|
||||
URL url = new URL(originalUrl);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
return responseCode == HttpURLConnection.HTTP_OK;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
127
src/main/java/com/bitmutex/shortener/UserController.java
Normal file
127
src/main/java/com/bitmutex/shortener/UserController.java
Normal file
|
@ -0,0 +1,127 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
|
||||
// UserController.java
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
@Autowired
|
||||
private OtpService otpService;
|
||||
|
||||
@Autowired
|
||||
private EmailService emailService;
|
||||
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<String> registerUser(@RequestBody RegistrationRequest registrationRequest, HttpServletRequest request) {
|
||||
try {
|
||||
String generatedOtp = otpService.generateAndStoreOtp(registrationRequest.getEmail());
|
||||
emailService.sendMail(registrationRequest.getEmail(),"OTP FOR REG VERIFICATION","OTP FPR REGISTRATION VERIFICATION IS:"+generatedOtp);
|
||||
UserEntity newUser = userService.registerNewUser(registrationRequest);
|
||||
|
||||
|
||||
URL url = new URL(request.getRequestURL().toString());
|
||||
String verificationUrl = new URL(url.getProtocol(), url.getHost(), url.getPort(), "").toString();
|
||||
verificationUrl.concat("?verify-registration?email="+registrationRequest.getEmail());
|
||||
|
||||
return ResponseEntity.ok("User registered successfully with ID: " + newUser.getId()+"\nplease verify your email at : "+verificationUrl);
|
||||
} catch (RegistrationException e) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred while processing the request.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/details/{username}")
|
||||
public ResponseEntity<UserDetailsDto> getUserDetails(@PathVariable String username) {
|
||||
try {
|
||||
UserDetailsDto userDetails = userService.getUserDetailsByUsername(username);
|
||||
return ResponseEntity.ok(userDetails);
|
||||
} catch (UsernameNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/update")
|
||||
public ResponseEntity<String> updateUserProfile(@RequestParam String username, @RequestBody UpdateUserRequest updateUserRequest) {
|
||||
// Save the updated user
|
||||
try {
|
||||
userService.updateUserDetailsByUsername(username,updateUserRequest);
|
||||
return ResponseEntity.ok("User profile updated successfully");
|
||||
} catch (UsernameNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
||||
}
|
||||
catch (RegistrationException e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("User Profile Update Failed as another user with same username already exists");
|
||||
}
|
||||
catch (DuplicateEmailException e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("User Profile Update Failed as another user with same email already exists");
|
||||
}
|
||||
catch (DuplicatePhoneNumberException e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("User Profile Update Failed as another user with same Phone Number already exists");
|
||||
}
|
||||
catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Unhandled Exception");
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/profile-picture")
|
||||
public ResponseEntity<String> updateUserProfilePicture(@RequestParam String username, @RequestParam("file") MultipartFile file) {
|
||||
// Save the updated user
|
||||
try {
|
||||
userService.updateUserProfilePictureByUsername(username,file);
|
||||
return ResponseEntity.ok("User profile picture updated successfully");
|
||||
} catch (UsernameNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/profile-picture")
|
||||
public ResponseEntity<byte[]> getUserProfilePicture(@RequestParam String username) {
|
||||
try {
|
||||
ResponseEntity<byte[]> image = userService.getProfilePictureByUsername(username);
|
||||
return image;
|
||||
} catch (UsernameNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/{userId}/username")
|
||||
public ResponseEntity<String> getUsernameById(@PathVariable Long userId) {
|
||||
String username = userService.getUsernameById(userId);
|
||||
|
||||
if (username != null) {
|
||||
return ResponseEntity.ok(username);
|
||||
} else {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
91
src/main/java/com/bitmutex/shortener/UserDetailsDto.java
Normal file
91
src/main/java/com/bitmutex/shortener/UserDetailsDto.java
Normal file
|
@ -0,0 +1,91 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
public class UserDetailsDto {
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String email;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public void setPhoneNumber(String phoneNumber) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
|
||||
private String phoneNumber;
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isAccountNonLocked() {
|
||||
return accountNonLocked;
|
||||
}
|
||||
|
||||
public void setAccountNonLocked(boolean accountNonLocked) {
|
||||
this.accountNonLocked = accountNonLocked;
|
||||
}
|
||||
|
||||
private boolean enabled;
|
||||
private boolean accountNonLocked;
|
||||
// Add other fields as needed
|
||||
|
||||
// Getters and setters
|
||||
|
||||
public byte[] getProfilePicture() {
|
||||
return profilePicture;
|
||||
}
|
||||
|
||||
public void setProfilePicture(byte[] profilePicture) {
|
||||
this.profilePicture = profilePicture;
|
||||
}
|
||||
|
||||
private byte[] profilePicture;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
UserEntity user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||
|
||||
|
||||
|
||||
return new CustomUserDetails(
|
||||
user.getUsername(),
|
||||
user.getPassword(),
|
||||
user.isEnabled(),
|
||||
user.isAccountNonLocked(),
|
||||
true, // Assuming credentials never expire; you can change this based on your requirements
|
||||
user.getEmail(),
|
||||
user.getFirstName(), // Include first name
|
||||
user.getLastName(), // Include last name
|
||||
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")),
|
||||
user.getId()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
193
src/main/java/com/bitmutex/shortener/UserEntity.java
Normal file
193
src/main/java/com/bitmutex/shortener/UserEntity.java
Normal file
|
@ -0,0 +1,193 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_entity")
|
||||
public class UserEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "user_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "username", unique = true, nullable = false)
|
||||
private String username;
|
||||
|
||||
@Column(name = "password", nullable = false)
|
||||
private String password;
|
||||
|
||||
@Column(name = "email", nullable = false)
|
||||
private String email;
|
||||
|
||||
@Column(name = "first_name")
|
||||
private String firstName;
|
||||
|
||||
@Column(name = "last_name")
|
||||
private String lastName;
|
||||
|
||||
@Column(name = "enabled")
|
||||
private boolean enabled = true;
|
||||
|
||||
public byte[] getProfilePicture() {
|
||||
return profilePicture;
|
||||
}
|
||||
|
||||
public void setProfilePicture(byte[] profilePicture) {
|
||||
this.profilePicture = profilePicture;
|
||||
}
|
||||
|
||||
@Column(name = "account_non_expired")
|
||||
private boolean accountNonExpired = true;
|
||||
|
||||
|
||||
// Fields for password reset
|
||||
@Column(name = "reset_token")
|
||||
private String resetToken;
|
||||
|
||||
@Column(name = "reset_token_expiry")
|
||||
private LocalDateTime resetTokenExpiryDateTime;
|
||||
|
||||
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public void setPhoneNumber(String phoneNumber) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
|
||||
@Lob
|
||||
@Column(name = "profile_picture", columnDefinition = "BLOB")
|
||||
private byte[] profilePicture;
|
||||
|
||||
@Column(name = "phone_number")
|
||||
private String phoneNumber;
|
||||
|
||||
|
||||
public String getResetToken() {
|
||||
return resetToken;
|
||||
}
|
||||
|
||||
public void setResetToken(String resetToken) {
|
||||
this.resetToken = resetToken;
|
||||
}
|
||||
|
||||
public LocalDateTime getResetTokenExpiryDateTime() {
|
||||
return resetTokenExpiryDateTime;
|
||||
}
|
||||
|
||||
public void setResetTokenExpiryDateTime(LocalDateTime resetTokenExpiryDateTime) {
|
||||
this.resetTokenExpiryDateTime = resetTokenExpiryDateTime;
|
||||
}
|
||||
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isAccountNonExpired() {
|
||||
return accountNonExpired;
|
||||
}
|
||||
|
||||
public void setAccountNonExpired(boolean accountNonExpired) {
|
||||
this.accountNonExpired = accountNonExpired;
|
||||
}
|
||||
|
||||
public boolean isAccountNonLocked() {
|
||||
return accountNonLocked;
|
||||
}
|
||||
|
||||
public void setAccountNonLocked(boolean accountNonLocked) {
|
||||
this.accountNonLocked = accountNonLocked;
|
||||
}
|
||||
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return credentialsNonExpired;
|
||||
}
|
||||
|
||||
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
|
||||
this.credentialsNonExpired = credentialsNonExpired;
|
||||
}
|
||||
|
||||
@Column(name = "account_non_locked")
|
||||
private boolean accountNonLocked = true;
|
||||
|
||||
@Column(name = "credentials_non_expired")
|
||||
private boolean credentialsNonExpired = true;
|
||||
|
||||
public SubscriptionPlan getSubscriptionPlan() {
|
||||
return subscriptionPlan;
|
||||
}
|
||||
|
||||
public void setSubscriptionPlan(SubscriptionPlan subscriptionPlan) {
|
||||
this.subscriptionPlan = subscriptionPlan;
|
||||
}
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "subscription_plan_id")
|
||||
private SubscriptionPlan subscriptionPlan;
|
||||
|
||||
|
||||
// Constructors, getters, and setters
|
||||
|
||||
// Getters and setters
|
||||
// Constructors, toString, equals, hashCode, etc.
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
public class UserNotVerifiedException extends AuthenticationException {
|
||||
|
||||
public UserNotVerifiedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
35
src/main/java/com/bitmutex/shortener/UserRepository.java
Normal file
35
src/main/java/com/bitmutex/shortener/UserRepository.java
Normal file
|
@ -0,0 +1,35 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository extends JpaRepository<UserEntity, Long> {
|
||||
|
||||
Optional<UserEntity> findByUsername(String username);
|
||||
|
||||
UserEntity findByEmail(String email);
|
||||
|
||||
@NotNull
|
||||
Optional<UserEntity> findById(@NotNull Long id);
|
||||
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
@Query("SELECT CASE WHEN COUNT(u) > 0 THEN true ELSE false END FROM UserEntity u WHERE u.email = :email AND u.username != :username")
|
||||
boolean existsByEmailAndUsernameNot(@Param("email") String email, @Param("username") String username);
|
||||
|
||||
@Query("SELECT CASE WHEN COUNT(u) > 0 THEN true ELSE false END FROM UserEntity u WHERE u.phoneNumber = :phoneNumber AND u.username != :username")
|
||||
boolean existsByPhoneNumberAndUsernameNot(@Param("phoneNumber") String phoneNumber, @Param("username") String username);
|
||||
|
||||
@Query("SELECT COUNT(u) > 0 FROM UserEntity u WHERE u.username = :username AND u.id <> :currentUserId")
|
||||
boolean existsByUsernameExcludingCurrentUser(@Param("username") String username, @Param("currentUserId") Long currentUserId);
|
||||
|
||||
|
||||
UserEntity findByResetToken(String resetToken);
|
||||
|
||||
}
|
34
src/main/java/com/bitmutex/shortener/UserService.java
Normal file
34
src/main/java/com/bitmutex/shortener/UserService.java
Normal file
|
@ -0,0 +1,34 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserService {
|
||||
|
||||
|
||||
UserEntity registerNewUser(RegistrationRequest registrationRequest);
|
||||
|
||||
UserEntity updateUserDetailsByUsername(String username, UpdateUserRequest updateUserRequest);
|
||||
|
||||
UserDetailsDto getUserDetailsByUsername(String username);
|
||||
|
||||
UserEntity save(UserEntity userEntity);
|
||||
|
||||
UserEntity findByEmail(String email);
|
||||
|
||||
@NotNull Optional<UserEntity> findById(Long id);
|
||||
|
||||
Optional<UserEntity> findByUsername(String username);
|
||||
|
||||
UserEntity findByResetToken(String resetToken);
|
||||
|
||||
UserEntity updateUserProfilePictureByUsername(String username, MultipartFile file) throws IOException;
|
||||
|
||||
ResponseEntity<byte[]> getProfilePictureByUsername (String username);
|
||||
String getUsernameById(Long userId);
|
||||
|
||||
}
|
238
src/main/java/com/bitmutex/shortener/UserServiceImpl.java
Normal file
238
src/main/java/com/bitmutex/shortener/UserServiceImpl.java
Normal file
|
@ -0,0 +1,238 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Optional;
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
@Autowired
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
@Autowired
|
||||
private EmailService emailService;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
|
||||
public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public UserEntity registerNewUser(RegistrationRequest registrationRequest) {
|
||||
UserEntity userEntity = null;
|
||||
try {
|
||||
// Check if the username or email is already taken (you may want to add more checks)
|
||||
if (userRepository.existsByUsername(registrationRequest.getUsername())) {
|
||||
throw new RegistrationException("Username is already taken");
|
||||
}
|
||||
|
||||
if (userRepository.existsByEmail(registrationRequest.getEmail())) {
|
||||
throw new RegistrationException("Email is already taken");
|
||||
}
|
||||
|
||||
// Create a new user entity
|
||||
userEntity = new UserEntity();
|
||||
userEntity.setUsername(registrationRequest.getUsername());
|
||||
userEntity.setPassword(passwordEncoder.encode(registrationRequest.getPassword()));
|
||||
userEntity.setEmail(registrationRequest.getEmail());
|
||||
userEntity.setFirstName(registrationRequest.getFirstName());
|
||||
userEntity.setLastName(registrationRequest.getLastName());
|
||||
userEntity.setEnabled(false);
|
||||
|
||||
// You can set additional fields and handle other registration logic here
|
||||
|
||||
// Save the user to the database
|
||||
userRepository.save(userEntity);
|
||||
|
||||
// Create a new subscription for the user
|
||||
subscriptionService.createSubscription(userEntity, "Free");
|
||||
|
||||
//Send Email
|
||||
String Subject = "Hi" + registrationRequest.getFirstName() + "Welcome to Link Shortener";
|
||||
String Message = "Welcome to Link Shortener, your username is :" + registrationRequest.getUsername();
|
||||
emailService.sendMail(registrationRequest.getEmail(), Subject, Message);
|
||||
|
||||
|
||||
log.info("Successfully registered user with username:"+userEntity.getUsername()+"and id : "+userEntity.getId());
|
||||
} catch (Exception ex) {
|
||||
log.error(ex.getMessage());
|
||||
}
|
||||
return userEntity;
|
||||
}
|
||||
|
||||
|
||||
public UserEntity updateUserProfilePictureByUsername(String username, MultipartFile file) {
|
||||
UserEntity user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||
|
||||
try {
|
||||
if (!file.isEmpty())
|
||||
{
|
||||
int sizeLimit = 5 * 1024 * 1024; // Set 5mb max limit
|
||||
if(file.getSize() > sizeLimit)
|
||||
throw new Exception("Filesize Limit Exceeded , File Size : " +file.getSize() + "Size Limit:"+sizeLimit);
|
||||
String contentType = file.getContentType();
|
||||
if (isValidImageFormat(contentType)) {
|
||||
user.setProfilePicture(file.getBytes());
|
||||
return userRepository.save(user);
|
||||
} else {
|
||||
log.error("Invalid file format for profile picture: " + contentType);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Profile Picture Update Failed ", e);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private boolean isValidImageFormat(String contentType) {
|
||||
return contentType != null && (contentType.equals(MediaType.IMAGE_PNG_VALUE) ||
|
||||
contentType.equals(MediaType.IMAGE_JPEG_VALUE) ||
|
||||
contentType.equals("image/webp") ||
|
||||
contentType.equals("image/bmp") ||
|
||||
contentType.equals("image/gif"));
|
||||
}
|
||||
|
||||
|
||||
public ResponseEntity<byte[]> getProfilePictureByUsername(String username) {
|
||||
UserEntity user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.IMAGE_PNG); // Set the appropriate content type
|
||||
|
||||
// Return the image data along with headers
|
||||
return new ResponseEntity<>(user.getProfilePicture(), headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public UserEntity updateUserDetailsByUsername(String username, UpdateUserRequest request) {
|
||||
|
||||
UserEntity user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||
|
||||
System.out.println(userRepository.existsByEmailAndUsernameNot(request.getEmail(), username));
|
||||
|
||||
// Check if another user with the same email already exists
|
||||
String newEmail = request.getEmail();
|
||||
if (newEmail != null && userRepository.existsByEmailAndUsernameNot(newEmail, username)) {
|
||||
log.error("Email Update Failed for user :" + username);
|
||||
throw new DuplicateEmailException("Another user with the same email already exists.");
|
||||
}
|
||||
|
||||
// Check if another user with the same phone already exists
|
||||
String phoneNumber = request.getPhoneNumber();
|
||||
if (phoneNumber != null && userRepository.existsByPhoneNumberAndUsernameNot(phoneNumber, username)) {
|
||||
log.error("phone number Update Failed for user :" + username);
|
||||
throw new DuplicatePhoneNumberException("Another user with the same Phone Number already exists.");
|
||||
}
|
||||
|
||||
|
||||
// Check if another user with the same username already exists excluding the current user
|
||||
Long userId = user.getId();
|
||||
if (username != null && userRepository.existsByUsernameExcludingCurrentUser(username,userId)) {
|
||||
log.error("Username Update Failed for user :" + username);
|
||||
throw new RegistrationException("Another user with the same Phone Number already exists.");
|
||||
}
|
||||
|
||||
|
||||
// Update optional fields if present
|
||||
//request.getProfilePicture().ifPresent(user::setProfilePicture);
|
||||
// request.getPhoneNumber().ifPresent(user::setPhoneNumber);
|
||||
// request.getEmail().ifPresent(user::setEmail); // Update other fields as needed
|
||||
|
||||
// Optional.ofNullable(request.getProfilePicture()).ifPresent(user::setProfilePicture);
|
||||
Optional.ofNullable(request.getPhoneNumber()).ifPresent(user::setPhoneNumber);
|
||||
Optional.ofNullable(request.getUsername()).ifPresent(user::setUsername);
|
||||
Optional.ofNullable(request.getPassword()).map(passwordEncoder::encode).ifPresent(user::setPassword);
|
||||
Optional.ofNullable(request.getEmail()).ifPresent(user::setEmail);
|
||||
Optional.ofNullable(request.getFirstName()).ifPresent(user::setFirstName);
|
||||
Optional.ofNullable(request.getLastName()).ifPresent(user::setLastName);
|
||||
|
||||
|
||||
/* if(request.getProfilePicture()!=null)
|
||||
user.setProfilePicture(request.getProfilePicture());
|
||||
if(request.getPhoneNumber()!=null)
|
||||
user.setPhoneNumber(request.getPhoneNumber());
|
||||
if(request.getPassword()!=null)
|
||||
user.setPassword ( passwordEncoder.encode ( request.getPassword() ) ); */
|
||||
userRepository.save(user);
|
||||
log.info("Successfully updated Profile records for user" + username);
|
||||
emailService.sendMail(request.getEmail(), "Profile Details Updated","You have successfully updated your profile details for username : "+username+"\n");
|
||||
return user;
|
||||
}
|
||||
|
||||
public UserDetailsDto getUserDetailsByUsername(String username) {
|
||||
UserEntity user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||
|
||||
UserDetailsDto userDetails = new UserDetailsDto();
|
||||
userDetails.setUserId(user.getId());
|
||||
userDetails.setUsername(user.getUsername());
|
||||
userDetails.setEmail(user.getEmail());
|
||||
userDetails.setFirstName(user.getFirstName());
|
||||
userDetails.setLastName(user.getLastName());
|
||||
userDetails.setEnabled(user.isEnabled());
|
||||
userDetails.setAccountNonLocked(true); // You can customize this based on your logic
|
||||
userDetails.setPhoneNumber(user.getPhoneNumber());
|
||||
userDetails.setProfilePicture(user.getProfilePicture());
|
||||
// Set other fields
|
||||
|
||||
return userDetails;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public UserEntity findByEmail(String email) {
|
||||
return userRepository.findByEmail(email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<UserEntity> findById(Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
@Override
|
||||
public Optional<UserEntity> findByUsername(String username) {
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public UserEntity save(UserEntity userEntity) {
|
||||
return userRepository.save(userEntity);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getUsernameById(Long userId) {
|
||||
// Implement logic to retrieve username by user ID from the data source
|
||||
Optional<UserEntity> userOptional = userRepository.findById(userId);
|
||||
return userOptional.map(UserEntity::getUsername).orElse(null);
|
||||
}
|
||||
@Override
|
||||
public UserEntity findByResetToken(String resetToken) {
|
||||
return userRepository.findByResetToken(resetToken);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package com.bitmutex.shortener;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.swing.text.html.Option;
|
||||
import java.util.Optional;
|
||||
|
||||
@Controller
|
||||
public class VerificationController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private OtpService otpService;
|
||||
|
||||
|
||||
|
||||
@GetMapping("/verify-registration")
|
||||
public String showVerificationPage(@RequestParam String email, Model model) {
|
||||
model.addAttribute("email", email);
|
||||
return "verify-registration";
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/verify-registration")
|
||||
public ResponseEntity<String> verifyRegistration(@RequestParam String otp, @RequestParam String email, Model model) {
|
||||
|
||||
//String email = model.getAttribute("email").toString();
|
||||
|
||||
Optional<String> storedOtp = otpService.getOtp(email);
|
||||
|
||||
// Verify OTP
|
||||
if (storedOtp.isPresent() && storedOtp.get().equals(otp)) {
|
||||
// OTP is valid, proceed with user registration
|
||||
UserEntity user = userRepository.findByEmail(email);
|
||||
user.setEnabled(true);
|
||||
otpService.removeOtpByEmail(email);
|
||||
userRepository.save(user);
|
||||
return ResponseEntity.ok("User verified successfully with ID: " +user.getId() +"\nyou can now login with username"+user.getUsername() );
|
||||
} else {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("you entered the wrong OTP");
|
||||
}
|
||||
}
|
||||
}
|
98
src/main/resources/application.properties
Normal file
98
src/main/resources/application.properties
Normal file
|
@ -0,0 +1,98 @@
|
|||
# Database settings
|
||||
database.port=3306
|
||||
database.name=shortener
|
||||
database.ip=127.0.0.1
|
||||
|
||||
# DataSource settings
|
||||
spring.datasource.url=jdbc:mysql://${database.ip}:${database.port}/${database.name}
|
||||
spring.datasource.username=root
|
||||
spring.datasource.password=
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
|
||||
|
||||
# Hibernate settings
|
||||
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
|
||||
|
||||
spring.jpa.open-in-view=false
|
||||
|
||||
# Set the RateLimit cooldown duration in seconds
|
||||
cooldown.duration=30
|
||||
|
||||
# Set the number of requests per second
|
||||
requests.per.second=1
|
||||
|
||||
#Context Path(start with / and not end with /)
|
||||
server.servlet.context-path=
|
||||
|
||||
#Server Port
|
||||
server.port=8080
|
||||
|
||||
#server base ur for qr code gen
|
||||
server.base.url= localhost:8080/
|
||||
|
||||
# Thymeleaf
|
||||
spring.thymeleaf.prefix=classpath:/templates/
|
||||
spring.thymeleaf.suffix=.html
|
||||
spring.thymeleaf.cache=false
|
||||
|
||||
|
||||
#logging level
|
||||
logging.level.org.springframework.security: DEBUG
|
||||
|
||||
#SCHEMAGEN
|
||||
spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create
|
||||
spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=shortener_db_schema.sql
|
||||
spring.jpa.properties.javax.persistence.schema-generation.scripts.create-source=metadata
|
||||
|
||||
#iframe
|
||||
spring.security.headers.frame=false
|
||||
|
||||
|
||||
#Email
|
||||
spring.mail.host=smtppro.zoho.in
|
||||
spring.mail.username=noreply@bitmutex.com
|
||||
spring.mail.password=g3BvVAHuxC9b
|
||||
spring.mail.port=587
|
||||
spring.mail.properties.mail.smtp.auth=true
|
||||
spring.mail.properties.mail.smtp.starttls.enable=true
|
||||
spring.mail.properties.mail.smtp.starttls.required=true
|
||||
|
||||
#
|
||||
application.base.url = http
|
||||
|
||||
#default error pages
|
||||
server.error.whitelabel.enabled=false
|
||||
server.error.path=/error
|
||||
|
||||
|
||||
|
||||
#Actuator Config Set security configuration for endpoints (customize as needed)
|
||||
management.endpoints.web.base-path=/actuator
|
||||
management.endpoints.web.exposure.include=*
|
||||
management.endpoint.health.show-details=always
|
||||
management.endpoint.info.enabled=true
|
||||
|
||||
#Server File Upload Limit
|
||||
spring.servlet.multipart.max-file-size=10MB
|
||||
spring.servlet.multipart.max-request-size=10MB
|
||||
|
||||
|
||||
|
||||
#OAuth2 param
|
||||
spring.security.oauth2.client.registration.github.client-id=Iv1.8d3d0ea51b7e7da3
|
||||
spring.security.oauth2.client.registration.github.client-secret=2176086761d073b2082afdc4af0207fa7d1d274b
|
||||
spring.security.oauth2.client.registration.github.scope=user:email
|
||||
|
||||
|
||||
#API Doc Path
|
||||
springdoc.api-docs.path=/docs
|
||||
#Swagger Path
|
||||
springdoc.swagger-ui.path=/docs-ui
|
||||
springdoc.swagger-ui.operationsSorter=method
|
||||
springdoc.show-actuator=true
|
||||
|
||||
springdoc.swagger-ui.oauthClientId=Iv1.8d3d0ea51b7e7da3
|
||||
springdoc.swagger-ui.oauthClientSecret=2176086761d073b2082afdc4af0207fa7d1d274b
|
||||
springdoc.swagger-ui.oauthAppName=Bitmutex Shortener
|
||||
springdoc.swagger-ui.oauthScopeSeparator=/v
|
||||
springdoc.swagger-ui.showRequestHeaders=true
|
22
src/main/resources/logback-spring.xml
Normal file
22
src/main/resources/logback-spring.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<!-- logback-spring.xml -->
|
||||
<configuration>
|
||||
<!-- File Appender -->
|
||||
<appender name="file" class="ch.qos.logback.core.FileAppender">
|
||||
<file>logs/shortener.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- Appender for console output -->
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- Root logger -->
|
||||
<root level="info">
|
||||
<appender-ref ref="file"/>
|
||||
</root>
|
||||
</configuration>
|
49
src/main/resources/static/js/ckeditor/LICENSE.md
Normal file
49
src/main/resources/static/js/ckeditor/LICENSE.md
Normal file
|
@ -0,0 +1,49 @@
|
|||
Software License Agreement
|
||||
==========================
|
||||
|
||||
Copyright (c) 2014-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
|
||||
Online builder code samples are licensed under the terms of the MIT License (see Appendix A):
|
||||
|
||||
http://en.wikipedia.org/wiki/MIT_License
|
||||
|
||||
CKEditor 5 collaboration features are only available under a commercial license. [Contact us](https://ckeditor.com/contact/) for more details.
|
||||
|
||||
Free 30-days trials of CKEditor 5 collaboration features are available:
|
||||
* https://ckeditor.com/collaboration/ - Real-time collaboration (with all features).
|
||||
* https://ckeditor.com/collaboration/comments/ - Inline comments feature (without real-time collaborative editing).
|
||||
* https://ckeditor.com/collaboration/track-changes/ - Track changes feature (without real-time collaborative editing).
|
||||
|
||||
Trademarks
|
||||
----------
|
||||
|
||||
CKEditor is a trademark of CKSource Holding sp. z o.o. All other brand
|
||||
and product names are trademarks, registered trademarks or service
|
||||
marks of their respective holders.
|
||||
|
||||
---
|
||||
|
||||
Appendix A: The MIT License
|
||||
---------------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2024, CKSource Holding sp. z o.o.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
60
src/main/resources/static/js/ckeditor/README.md
Normal file
60
src/main/resources/static/js/ckeditor/README.md
Normal file
|
@ -0,0 +1,60 @@
|
|||
# CKEditor 5 editor generated with the online builder
|
||||
|
||||
This repository presents a CKEditor 5 editor build generated by the [Online builder tool](https://ckeditor.com/ckeditor-5/online-builder)
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Open the `sample/index.html` page in the browser.
|
||||
|
||||
2. Fill the prompt with the license key. If you do not have the license key yet [contact us](https://ckeditor.com/contact/).
|
||||
|
||||
## Configuring build
|
||||
|
||||
Changes like changing toolbar items, changing order of icons or customizing plugin configurations should be relatively easy to make. Open the `sample/index.html` file and edit the script that initialized the CKEditor 5. Save the file and refresh the browser. That's all.
|
||||
|
||||
*Note:* If you have any problems with browser caching use the `Ctrl + R` or `Cmd + R` shortcut depending on your system.
|
||||
|
||||
However if you want to remove or add a plugin to the build you need to follow the next step of this guide.
|
||||
|
||||
Note that it is also possible to go back to the [Online builder tool](https://ckeditor.com/ckeditor-5/online-builder) and pick other set of plugins. But we encourage you to try the harder way and to learn the principles of Node.js and CKEditor 5 ecosystems that will allow you to do more cool things in the future!
|
||||
|
||||
### Installation
|
||||
|
||||
In order to rebuild the application you need to install all dependencies first. To do it, open the terminal in the project directory and type:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Make sure that you have the `node` and `npm` installed first. If not, then follow the instructions on the [Node.js documentation page](https://nodejs.org/en/).
|
||||
|
||||
### Adding or removing plugins
|
||||
|
||||
Now you can install additional plugin in the build. Just follow the [Adding a plugin to an editor tutorial](https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/installing-plugins.html#adding-a-plugin-to-an-editor)
|
||||
|
||||
### Rebuilding editor
|
||||
|
||||
If you have already done the [Installation](#installation) and [Adding or removing plugins](#adding-or-removing-plugins) steps, you're ready to rebuild the editor by running the following command:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
This will build the CKEditor 5 to the `build` directory. You can open your browser and you should be able to see the changes you've made in the code. If not, then try to refresh also the browser cache by typing `Ctrl + R` or `Cmd + R` depending on your system.
|
||||
|
||||
## What's next?
|
||||
|
||||
Follow the guides available on https://ckeditor.com/docs/ckeditor5/latest/framework/index.html and enjoy the document editing.
|
||||
|
||||
## FAQ
|
||||
| Where is the place to report bugs and feature requests?
|
||||
|
||||
You can create an issue on https://github.com/ckeditor/ckeditor5/issues including the build id - `r3nnrlu9zfkf-8by2n9g31wm7`. Make sure that the question / problem is unique, please look for a possibly asked questions in the search box. Duplicates will be closed.
|
||||
|
||||
| Where can I learn more about the CKEditor 5 framework?
|
||||
|
||||
Here: https://ckeditor.com/docs/ckeditor5/latest/framework/
|
||||
|
||||
| Is it possible to use online builder with common frameworks like React, Vue or Angular?
|
||||
|
||||
Not yet, but it these integrations will be available at some point in the future.
|
48
src/main/resources/static/js/ckeditor/build/ckeditor.d.ts
vendored
Normal file
48
src/main/resources/static/js/ckeditor/build/ckeditor.d.ts
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* @license Copyright (c) 2014-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
|
||||
import { Alignment } from '@ckeditor/ckeditor5-alignment';
|
||||
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';
|
||||
import { Autosave } from '@ckeditor/ckeditor5-autosave';
|
||||
import { Bold, Code, Italic, Strikethrough, Subscript, Superscript } from '@ckeditor/ckeditor5-basic-styles';
|
||||
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote';
|
||||
import { CodeBlock } from '@ckeditor/ckeditor5-code-block';
|
||||
import type { EditorConfig } from '@ckeditor/ckeditor5-core';
|
||||
import { Essentials } from '@ckeditor/ckeditor5-essentials';
|
||||
import { FindAndReplace } from '@ckeditor/ckeditor5-find-and-replace';
|
||||
import { FontBackgroundColor, FontColor, FontFamily, FontSize } from '@ckeditor/ckeditor5-font';
|
||||
import { Heading, Title } from '@ckeditor/ckeditor5-heading';
|
||||
import { Highlight } from '@ckeditor/ckeditor5-highlight';
|
||||
import { HorizontalLine } from '@ckeditor/ckeditor5-horizontal-line';
|
||||
import { HtmlEmbed } from '@ckeditor/ckeditor5-html-embed';
|
||||
import { DataFilter, DataSchema, GeneralHtmlSupport, HtmlComment } from '@ckeditor/ckeditor5-html-support';
|
||||
import { AutoImage, Image, ImageCaption, ImageInsert, ImageResize, ImageStyle, ImageToolbar, ImageUpload } from '@ckeditor/ckeditor5-image';
|
||||
import { Indent, IndentBlock } from '@ckeditor/ckeditor5-indent';
|
||||
import { TextPartLanguage } from '@ckeditor/ckeditor5-language';
|
||||
import { AutoLink, Link, LinkImage } from '@ckeditor/ckeditor5-link';
|
||||
import { List, ListProperties, TodoList } from '@ckeditor/ckeditor5-list';
|
||||
import { Markdown } from '@ckeditor/ckeditor5-markdown-gfm';
|
||||
import { MediaEmbed, MediaEmbedToolbar } from '@ckeditor/ckeditor5-media-embed';
|
||||
import { Mention } from '@ckeditor/ckeditor5-mention';
|
||||
import { PageBreak } from '@ckeditor/ckeditor5-page-break';
|
||||
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
|
||||
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
|
||||
import { RemoveFormat } from '@ckeditor/ckeditor5-remove-format';
|
||||
import { StandardEditingMode } from '@ckeditor/ckeditor5-restricted-editing';
|
||||
import { SelectAll } from '@ckeditor/ckeditor5-select-all';
|
||||
import { ShowBlocks } from '@ckeditor/ckeditor5-show-blocks';
|
||||
import { SourceEditing } from '@ckeditor/ckeditor5-source-editing';
|
||||
import { SpecialCharacters, SpecialCharactersArrows, SpecialCharactersCurrency, SpecialCharactersEssentials, SpecialCharactersLatin, SpecialCharactersMathematical, SpecialCharactersText } from '@ckeditor/ckeditor5-special-characters';
|
||||
import { Style } from '@ckeditor/ckeditor5-style';
|
||||
import { Table, TableCaption, TableCellProperties, TableColumnResize, TableProperties, TableToolbar } from '@ckeditor/ckeditor5-table';
|
||||
import { TextTransformation } from '@ckeditor/ckeditor5-typing';
|
||||
import { Undo } from '@ckeditor/ckeditor5-undo';
|
||||
import { Base64UploadAdapter } from '@ckeditor/ckeditor5-upload';
|
||||
import { WordCount } from '@ckeditor/ckeditor5-word-count';
|
||||
declare class Editor extends ClassicEditor {
|
||||
static builtinPlugins: (typeof Alignment | typeof AutoImage | typeof AutoLink | typeof Autoformat | typeof Autosave | typeof Base64UploadAdapter | typeof BlockQuote | typeof Bold | typeof Code | typeof CodeBlock | typeof DataFilter | typeof DataSchema | typeof Essentials | typeof FindAndReplace | typeof FontBackgroundColor | typeof FontColor | typeof FontFamily | typeof FontSize | typeof GeneralHtmlSupport | typeof Heading | typeof Highlight | typeof HorizontalLine | typeof HtmlComment | typeof HtmlEmbed | typeof Image | typeof ImageCaption | typeof ImageInsert | typeof ImageResize | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof Indent | typeof IndentBlock | typeof Italic | typeof Link | typeof LinkImage | typeof List | typeof ListProperties | typeof Markdown | typeof MediaEmbed | typeof MediaEmbedToolbar | typeof Mention | typeof PageBreak | typeof Paragraph | typeof PasteFromOffice | typeof RemoveFormat | typeof SelectAll | typeof ShowBlocks | typeof SourceEditing | typeof SpecialCharacters | typeof SpecialCharactersArrows | typeof SpecialCharactersCurrency | typeof SpecialCharactersEssentials | typeof SpecialCharactersLatin | typeof SpecialCharactersMathematical | typeof SpecialCharactersText | typeof StandardEditingMode | typeof Strikethrough | typeof Style | typeof Subscript | typeof Superscript | typeof Table | typeof TableCaption | typeof TableCellProperties | typeof TableColumnResize | typeof TableProperties | typeof TableToolbar | typeof TextPartLanguage | typeof TextTransformation | typeof Title | typeof TodoList | typeof Undo | typeof WordCount)[];
|
||||
static defaultConfig: EditorConfig;
|
||||
}
|
||||
export default Editor;
|
6
src/main/resources/static/js/ckeditor/build/ckeditor.js
vendored
Normal file
6
src/main/resources/static/js/ckeditor/build/ckeditor.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"ckeditor.js","mappings":";;;;AAAA","sources":["webpack://ClassicEditor/webpack/universalModuleDefinition"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClassicEditor\"] = factory();\n\telse\n\t\troot[\"ClassicEditor\"] = factory();\n})(self, () => {\nreturn "],"names":[],"sourceRoot":""}
|
|
@ -0,0 +1 @@
|
|||
(function(e){const n=e["af"]=e["af"]||{};n.dictionary=Object.assign(n.dictionary||{},{"%0 of %1":"%0 van %1","Align center":"Belyn in die middel","Align left":"Belyn links","Align right":"Belyn regs","Block quote":"Verwysingsaanhaling",Bold:"Vet",Cancel:"Kanselleer",Clear:"",Code:"Bronkode",Find:"Soek","Find and replace":"Soek en vervang","Find in text…":"Soek in teks …","Insert code block":"Voeg bronkodeblok in",Italic:"Kursief",Justify:"Belyn beide kante","Match case":"Hooflettersensitief","Next result":"Volgende resultaat","Plain text":"Gewone skrif","Previous result":"Vorige resultaat","Remove color":"Verwyder kleur","Remove Format":"Verwyder formatering",Replace:"Vervang","Replace all":"Vervang alles","Replace with…":"Vervang met ...","Restore default":"Herstel verstek",Save:"Stoor","Saving changes":"Veranderinge word gestoor","Show more items":"Wys meer items","Show options":"Wys opsies",Strikethrough:"Deurstreep",Subscript:"Onderskrif",Superscript:"Boskrif","Text alignment":"Teksbelyning","Text alignment toolbar":"Teksbelyning nutsbank","Text to find must not be empty.":"Soekteks mag nie leeg wees nie.","Tip: Find some text first in order to replace it.":"Wenk: Soek eers 'n bietjie teks om dit te vervang.",Underline:"Onderstreep","Whole words only":"Slegs hele woorde"});n.getPluralForm=function(e){return e!=1}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
(function(e){const t=e["ast"]=e["ast"]||{};t.dictionary=Object.assign(t.dictionary||{},{"%0 of %1":"",Accept:"",Aquamarine:"",Black:"",Blue:"",Bold:"Negrina","Break text":"","Bulleted List":"Llista con viñetes","Bulleted list styles toolbar":"",Cancel:"Encaboxar","Caption for image: %0":"","Caption for the image":"","Centered image":"","Change image text alternative":"",Circle:"",Clear:"","Click to edit block":"",Code:"",Decimal:"","Decimal with leading zero":"","Dim grey":"",Disc:"",Downloadable:"","Drag to move":"","Dropdown toolbar":"","Edit block":"","Edit link":"","Editor block content toolbar":"","Editor contextual toolbar":"","Editor editing area: %0":"","Editor toolbar":"","Enter image caption":"","Full size image":"Imaxen a tamañu completu",Green:"",Grey:"",HEX:"","Image resize list":"","Image toolbar":"","image widget":"complementu d'imaxen","In line":"",Insert:"","Insert image":"","Insert image via URL":"",Italic:"Cursiva","Left aligned image":"","Light blue":"","Light green":"","Light grey":"",Link:"Enllazar","Link image":"","Link URL":"URL del enllaz","List properties":"","Lower-latin":"","Lower–roman":"",Next:"","No results found":"","No searchable items":"","Numbered List":"Llista numberada","Numbered list styles toolbar":"","Open in a new tab":"","Open link in new tab":"",Orange:"",Original:"",Previous:"",Purple:"",Red:"",Redo:"Refacer","Remove color":"","Replace from computer":"","Replace image":"","Replace image from computer":"","Resize image":"","Resize image to %0":"","Resize image to the original size":"","Restore default":"","Reversed order":"","Rich Text Editor":"Editor de testu arriquecíu","Right aligned image":"",Save:"Guardar","Show more items":"","Side image":"Imaxen llateral",Square:"","Start at":"","Start index must be greater than 0.":"",Strikethrough:"",Subscript:"",Superscript:"","Text alternative":"","This link has no URL":"","To-do List":"","Toggle the circle list style":"","Toggle the decimal list style":"","Toggle the decimal with leading zero list style":"","Toggle the disc list style":"","Toggle the lower–latin list style":"","Toggle the lower–roman list style":"","Toggle the square list style":"","Toggle the upper–latin list style":"","Toggle the upper–roman list style":"",Turquoise:"",Underline:"",Undo:"Desfacer",Unlink:"Desenllazar",Update:"","Update image URL":"","Upload failed":"","Upload from computer":"","Upload image from computer":"","Upper-latin":"","Upper-roman":"",White:"","Wrap text":"",Yellow:""});t.getPluralForm=function(e){return e!=1}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
(function(e){const a=e["bs"]=e["bs"]||{};a.dictionary=Object.assign(a.dictionary||{},{"%0 of %1":"%0 od %1","Align center":"Centrirati","Align left":"Lijevo poravnanje","Align right":"Desno poravnanje",Big:"","Block quote":"Citat",Bold:"Podebljano","Break text":"",Cancel:"Poništi","Caption for image: %0":"","Caption for the image":"","Centered image":"Centrirana slika","Change image text alternative":"Promijeni ALT atribut za sliku","Choose heading":"Odaberi naslov",Clear:"",Code:"Kod",Default:"Zadani","Document colors":"","Edit source":"Uredi izvor","Empty snippet content":"HTML odlomak nema sadžaj","Enter image caption":"Unesi naziv slike",Find:"Pronađi","Find and replace":"Pronađi i zamijeni","Find in text…":"Pronađi u tekstu","Font Background Color":"Boja pozadine","Font Color":"Boja","Font Family":"Font","Font Size":"Veličina fonta","Full size image":"",Heading:"Naslov","Heading 1":"Naslov 1","Heading 2":"Naslov 2","Heading 3":"Naslov 3","Heading 4":"Naslov 4","Heading 5":"Naslov 5","Heading 6":"Naslov 6","Horizontal line":"Horizontalna linija","HTML snippet":"HTML odlomak",Huge:"","Image resize list":"Lista veličina slike","Image toolbar":"","image widget":"","In line":"",Insert:"Umetni","Insert code block":"Umetni kod blok","Insert HTML":"Umetni HTML","Insert image":"Umetni sliku","Insert image via URL":"Umetni sliku preko URLa",Italic:"Zakrivljeno",Justify:"","Left aligned image":"Lijevo poravnata slika","Match case":"Podudaranje","Next result":"","No preview available":"Pregled nedostupan",Original:"Original",Paragraph:"Paragraf","Paste raw HTML here...":"Zalijepi HTML ovdje...","Plain text":"Tekst","Previous result":"Prethodni rezultat","Remove color":"Ukloni boju",Replace:"Zamijeni","Replace all":"Zamijeni sve","Replace from computer":"","Replace image":"","Replace image from computer":"","Replace with…":"Zamijeni sa...","Resize image":"Promijeni veličinu slike","Resize image to %0":"","Resize image to the original size":"Postavi originalnu veličinu slike","Restore default":"Vrati na zadano","Right aligned image":"Desno poravnata slika",Save:"Sačuvaj","Save changes":"Sačuvaj izmjene","Saving changes":"Spremanje izmjena","Show more items":"Prikaži više stavki","Show options":"Prikaži opcije","Side image":"",Small:"",Strikethrough:"Precrtano",Subscript:"",Superscript:"","Text alignment":"Poravnanje teksta","Text alignment toolbar":"Traka za poravnanje teksta","Text alternative":"ALT atribut","Text to find must not be empty.":"Unesite tekst za pretragu.",Tiny:"","Tip: Find some text first in order to replace it.":"","Type or paste your content here.":"Unesite ili zalijepite vaš sadržaj ovdje","Type your title":"Unesite naslov",Underline:"Podcrtano",Update:"Ažuriraj","Update image URL":"Ažuriraj URL slike","Upload failed":"Učitavanje slike nije uspjelo","Upload from computer":"","Upload image from computer":"","Whole words only":"Samo cijele riječi","Wrap text":"Prelomi tekst"});a.getPluralForm=function(e){return e%10==1&&e%100!=11?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue