This commit is contained in:
Amit Kumar Nandi 2024-02-01 23:44:46 +05:30
commit ff528b7017
268 changed files with 336913 additions and 0 deletions

33
.gitignore vendored Normal file
View 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

Binary file not shown.

2
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

303578
logs/shortener.log Normal file

File diff suppressed because it is too large Load diff

308
mvnw vendored Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

View 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...
}

View file

@ -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
}
}
}

View file

@ -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);
}

View 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);
}
}

View 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";
}
}

View 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";
}
}
}

View 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() {
}
}

View file

@ -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
}

View file

@ -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");
}
}

View 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
}

View file

@ -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;
}
}

View 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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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>&copy; 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();
}
}
}

View file

@ -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";
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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";
}
}

View 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;
}
}

View 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;
}
}

View file

@ -0,0 +1,6 @@
package com.bitmutex.shortener;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ImageRepository extends JpaRepository<ImageEntity, Long> {
}

View 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);
}

View 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;
}
}
}

View 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
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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";
}
}

View 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;
}
}

View 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);
}

View 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);
}
}
}

View file

@ -0,0 +1,5 @@
package com.bitmutex.shortener;
public interface PaymentService {
boolean verifyPayment(RazorpayPaymentDetails paymentDetails);
}

View 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";
}
}

View file

@ -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);
}
}

View 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
}
}

View file

@ -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.
}

View file

@ -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);
}
}

View file

@ -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
}

View file

@ -0,0 +1,7 @@
package com.bitmutex.shortener;
public class RegistrationResponse {
private String message;
// getters and setters
}

View file

@ -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";
}
}
}

View 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();
}
}

View 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);
}
}
}

View file

@ -0,0 +1,8 @@
package com.bitmutex.shortener;
public enum SourceType {
WEBSITE,
FRIEND,
SOCIAL_MEDIA,
OTHER
}

View file

@ -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);
}
}

View 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
}
}
}

View 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.
}

View file

@ -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");
}
}
}

View file

@ -0,0 +1,7 @@
package com.bitmutex.shortener;
public class SubscriptionNotFoundException extends RuntimeException {
public SubscriptionNotFoundException(String message) {
super(message);
}
}

View file

@ -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.
}

View file

@ -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
}

View 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
}

View file

@ -0,0 +1,7 @@
package com.bitmutex.shortener;
public class SubscriptionPlanNotFoundException extends RuntimeException {
public SubscriptionPlanNotFoundException(String message) {
super(message);
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;
}
}

View 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;
}

View file

@ -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";
}
}

View 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
}

View file

@ -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>";
}
}

View 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 + '\'' +
'}';
}
}

View file

@ -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);
}
}

View 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
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View 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;
}
}
}

View 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();
}
}
}

View 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;
}

View file

@ -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()
);
}
}

View 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.
}

View file

@ -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);
}
}

View 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);
}

View 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);
}

View 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);
}
}

View file

@ -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");
}
}
}

View 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

View 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>

View 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.

View 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.

View 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;

File diff suppressed because one or more lines are too long

View file

@ -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":""}

View file

@ -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

View file

@ -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":"","Lowerroman":"",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 lowerlatin list style":"","Toggle the lowerroman list style":"","Toggle the square list style":"","Toggle the upperlatin list style":"","Toggle the upperroman 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

View file

@ -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