Initial commit
This commit is contained in:
commit
2550140811
|
|
@ -0,0 +1,41 @@
|
|||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Kotlin ###
|
||||
.kotlin
|
||||
.env*
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
plugins {
|
||||
id("org.springframework.boot") version "3.3.0"
|
||||
id("io.spring.dependency-management") version "1.1.5"
|
||||
kotlin("plugin.jpa") version "1.9.24"
|
||||
kotlin("jvm") version "1.9.24"
|
||||
kotlin("plugin.spring") version "1.9.24"
|
||||
kotlin("plugin.allopen") version "1.9.22"
|
||||
}
|
||||
|
||||
group = "at.eisibaer"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom(configurations.annotationProcessor.get())
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
val jjwtVersion: String = "0.12.6";
|
||||
val bcVersion: String = "1.78.1";
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-websocket")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion")
|
||||
implementation("org.bouncycastle:bcprov-jdk18on:$bcVersion")
|
||||
compileOnly("org.projectlombok:lombok")
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:$jjwtVersion")
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jjwtVersion")
|
||||
annotationProcessor("org.projectlombok:lombok")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||
testImplementation("org.springframework.security:spring-security-test")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xjsr305=strict")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
allOpen {
|
||||
annotation("jakarta.persistence.Entity")
|
||||
annotation("jakarta.persistence.Embeddable")
|
||||
annotation("jakarta.persistence.MappedSuperclass")
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
@ -0,0 +1 @@
|
|||
rootProject.name = "jeobeardy"
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package at.eisibaer.jbear2
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan(basePackages = ["at.eisibaer.jbear2.config"])
|
||||
class JeobeardyApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<JeobeardyApplication>(*args)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package at.eisibaer.jbear2.config
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
|
||||
@ConfigurationProperties("application")
|
||||
data class ApplicationProperties(
|
||||
val corsAllowedOrigins: List<String>,
|
||||
val corsAllowedMethods: List<String>,
|
||||
val jwtCookieName: String,
|
||||
val jwtExpirationMs: Long,
|
||||
val jwtSecret: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package at.eisibaer.jbear2.config
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.io.Resource
|
||||
import org.springframework.lang.Nullable
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver
|
||||
import org.springframework.web.servlet.resource.ResourceResolverChain
|
||||
|
||||
@Configuration
|
||||
class SpringConfiguration(
|
||||
private val applicationProperties: ApplicationProperties
|
||||
) : WebMvcConfigurer {
|
||||
|
||||
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
|
||||
this.serveDirectory(registry, "/", "classpath:/static/")
|
||||
}
|
||||
|
||||
private fun serveDirectory(registry: ResourceHandlerRegistry, endpoint: String, location: String){
|
||||
val endpointPatterns: Array<String>;
|
||||
if (endpoint.endsWith("/") ){
|
||||
endpointPatterns = arrayOf(endpoint.substring(0, endpoint.length - 1), endpoint, "$endpoint**")
|
||||
} else {
|
||||
endpointPatterns = arrayOf(endpoint, "$endpoint/", "$endpoint/**")
|
||||
}
|
||||
registry
|
||||
.addResourceHandler(*endpointPatterns)
|
||||
.addResourceLocations( if( location.endsWith("/") ) location else "$location/")
|
||||
.resourceChain(true)
|
||||
.addResolver(object : PathResourceResolver(){
|
||||
override fun resolveResource( @Nullable request: HttpServletRequest?, requestPath: String, locations: MutableList<out Resource>, chain: ResourceResolverChain ): Resource? {
|
||||
val resource: Resource? = super.resolveResource(request, requestPath, locations, chain);
|
||||
if( resource != null ){
|
||||
return resource;
|
||||
}
|
||||
return super.resolveResource(request, "/index.html", locations, chain);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
registry
|
||||
.addMapping("/api/**")
|
||||
.allowedOrigins(*applicationProperties.corsAllowedOrigins.map { it }.toTypedArray())
|
||||
.allowedMethods(*applicationProperties.corsAllowedMethods.map { it }.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package at.eisibaer.jbear2.config
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
|
||||
|
||||
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
|
||||
registry.enableSimpleBroker("/game");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
|
||||
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
|
||||
registry.addEndpoint("/websocket")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package at.eisibaer.jbear2.dto.auth
|
||||
|
||||
data class LoginDto(
|
||||
val username: String,
|
||||
val password: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package at.eisibaer.jbear2.dto.auth
|
||||
|
||||
data class LoginResponseDto(
|
||||
val username: String,
|
||||
val profilePicture: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package at.eisibaer.jbear2.dto.message
|
||||
|
||||
import lombok.AllArgsConstructor
|
||||
|
||||
@AllArgsConstructor
|
||||
data class GenericMessage (
|
||||
|
||||
var name: String? = "",
|
||||
|
||||
var content: String? = "",
|
||||
)
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package at.eisibaer.jbear2.endpoint
|
||||
|
||||
import at.eisibaer.jbear2.dto.auth.LoginDto
|
||||
import at.eisibaer.jbear2.dto.auth.LoginResponseDto
|
||||
import at.eisibaer.jbear2.model.Board
|
||||
import at.eisibaer.jbear2.model.User
|
||||
import at.eisibaer.jbear2.repository.UserRepository
|
||||
import at.eisibaer.jbear2.security.jwt.JwtUtils
|
||||
import at.eisibaer.jbear2.security.userdetail.UserDetailsImpl
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.ResponseCookie
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/api/auth")
|
||||
class AuthEndpoint(
|
||||
val authenticationManager: AuthenticationManager,
|
||||
val userRepository: UserRepository,
|
||||
val encoder: PasswordEncoder,
|
||||
val jwtUtils: JwtUtils,
|
||||
) {
|
||||
|
||||
private val log: Logger = LoggerFactory.getLogger(AuthEndpoint::class.java);
|
||||
|
||||
val strResponseSuccess: String = "Sending back success response";
|
||||
|
||||
@PostMapping("/signup")
|
||||
fun signupUser(@RequestBody loginDto: LoginDto): ResponseEntity<String>{
|
||||
log.info("Endpoint singupUser called");
|
||||
log.debug("signup Request with username: {}", loginDto.username);
|
||||
if( userRepository.existsByUsername(loginDto.username)){
|
||||
log.info("Username was already taken");
|
||||
ResponseEntity.badRequest().body("Username already taken");
|
||||
}
|
||||
|
||||
val user = User(loginDto.username, encoder.encode( loginDto.password), ArrayList(), null, null );
|
||||
|
||||
userRepository.save(user);
|
||||
|
||||
log.info(strResponseSuccess);
|
||||
return ResponseEntity.ok().body("User registered successfully");
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
fun loginUser(@RequestBody loginDto: LoginDto): ResponseEntity<LoginResponseDto>{
|
||||
log.info("Endpoint loginUser called");
|
||||
log.debug("login Request with username: {}", loginDto.username);
|
||||
val authentication = authenticationManager.authenticate(
|
||||
UsernamePasswordAuthenticationToken(
|
||||
loginDto.username,
|
||||
loginDto.password
|
||||
)
|
||||
)
|
||||
|
||||
SecurityContextHolder.getContext().authentication = authentication;
|
||||
|
||||
val userDetails: UserDetailsImpl = authentication.principal as UserDetailsImpl;
|
||||
|
||||
val jwtCookie = jwtUtils.generateJwtCookie(userDetails);
|
||||
|
||||
log.info(strResponseSuccess);
|
||||
return ResponseEntity.ok()
|
||||
.header( HttpHeaders.SET_COOKIE, jwtCookie.toString() )
|
||||
.body( LoginResponseDto(userDetails.username, userDetails.getProfilePictureFilename()))
|
||||
}
|
||||
|
||||
@PostMapping("signout")
|
||||
fun logoutUser(): ResponseEntity<String>{
|
||||
log.info("Endpoint logoutUser called");
|
||||
val cookie: ResponseCookie = jwtUtils.getCleanJwtCookie();
|
||||
|
||||
log.info(strResponseSuccess);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.SET_COOKIE, cookie.toString())
|
||||
.body("Logged out");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package at.eisibaer.jbear2.endpoint
|
||||
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/api/board")
|
||||
class BoardEndpoint {
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package at.eisibaer.jbear2.endpoint
|
||||
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping
|
||||
import org.springframework.messaging.handler.annotation.SendTo
|
||||
|
||||
import at.eisibaer.jbear2.dto.message.GenericMessage
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/game")
|
||||
class GameEndpoint {
|
||||
|
||||
@MessageMapping("/player/join")
|
||||
@SendTo("/player/joined")
|
||||
fun playerJoining(playerJoiningMessage: GenericMessage): GenericMessage{
|
||||
return playerJoiningMessage.copy(playerJoiningMessage.name, "Joined");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package at.eisibaer.jbear2.endpoint
|
||||
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
class UserEndpoint {
|
||||
|
||||
val log: Logger = LoggerFactory.getLogger(UserEndpoint::class.java);
|
||||
|
||||
@GetMapping("/test/{pathVar}")
|
||||
fun testEndpoint(@PathVariable pathVar: String, @RequestParam param1: String): ResponseEntity<String>{
|
||||
log.info("test Endpoint!");
|
||||
return ResponseEntity.ok(param1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package at.eisibaer.jbear2.model
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "answers", indexes = [
|
||||
Index(name = "fk_board_entry_to_answer", columnList = "fk_board_entry")
|
||||
])
|
||||
data class Answer(
|
||||
|
||||
@Column(name = "text", nullable = false, unique = false)
|
||||
val text: String,
|
||||
|
||||
@OneToOne
|
||||
@JoinColumn(name = "fk_image_file", referencedColumnName = "id")
|
||||
val image: ImageFile?,
|
||||
|
||||
@OneToOne
|
||||
@JoinColumn(name="fk_board_entry", referencedColumnName = "id")
|
||||
val boardEntry: BoardEntry?,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
val id: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package at.eisibaer.jbear2.model
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "boards", indexes = [
|
||||
Index(name = "fk_owner_of_board", columnList = "fk_owned_by")
|
||||
])
|
||||
data class Board(
|
||||
|
||||
@OneToMany(mappedBy = "board")
|
||||
val categories: List<Category>,
|
||||
|
||||
@Column(name = "board_name", nullable = false, unique = false)
|
||||
val boardName: String,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name="fk_owned_by", referencedColumnName = "id")
|
||||
val owner: User,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
val id: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package at.eisibaer.jbear2.model
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "board_entries", indexes = [
|
||||
Index(name = "fk_category_to_board_entry", columnList = "fk_category")
|
||||
])
|
||||
data class BoardEntry(
|
||||
|
||||
@Column(name = "name", nullable = false, unique = false)
|
||||
val name: String,
|
||||
|
||||
@OneToMany(mappedBy = "boardEntry")
|
||||
val questions: List<Question>,
|
||||
|
||||
@OneToOne
|
||||
val answer: Answer,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "fk_category", referencedColumnName = "id")
|
||||
val category: Category?,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
val id: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package at.eisibaer.jbear2.model
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "categories", indexes = [
|
||||
Index(name = "fk_category_to_board", columnList = "fk_board")
|
||||
])
|
||||
data class Category (
|
||||
|
||||
@Column(name = "name", nullable = false, unique = false)
|
||||
val name: String,
|
||||
|
||||
@Column(name = "description", nullable = false, unique = false)
|
||||
val description: String,
|
||||
|
||||
@OneToMany(mappedBy = "category")
|
||||
val boardEntries: List<BoardEntry>,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "fk_board", referencedColumnName = "id")
|
||||
val board: Board,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
val id: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package at.eisibaer.jbear2.model
|
||||
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
@Table(name = "games", indexes = [
|
||||
Index(name = "fk_board_to_game", columnList = "fk_board"),
|
||||
Index(name = "fk_host_to_game", columnList = "fk_host_user"),
|
||||
Index(name = "fk_choosing_player_to_game", columnList = "fk_player_currently_choosing")
|
||||
])
|
||||
data class Game(
|
||||
|
||||
@Column(name = "invite_code", nullable = false, unique = true)
|
||||
val inviteCode: String,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "fk_board", referencedColumnName = "id")
|
||||
val board: Board,
|
||||
|
||||
@OneToOne
|
||||
@JoinColumn(name="fk_host_user", referencedColumnName = "id")
|
||||
val host: User,
|
||||
|
||||
@OneToMany(mappedBy = "currentGame")
|
||||
val players: List<Player>,
|
||||
|
||||
@OneToOne
|
||||
@JoinColumn(name="fk_player_currently_choosing", referencedColumnName = "id")
|
||||
val currentlyChoosingPlayer: Player,
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "answered_board_entries_in_games",
|
||||
joinColumns = [JoinColumn(name = "fk_game")],
|
||||
inverseJoinColumns = [JoinColumn(name = "fk_board_entry")]
|
||||
)
|
||||
val alreadyAnsweredEntries: List<BoardEntry>,
|
||||
|
||||
@Column(name = "accepting_answers", nullable = false, unique = false)
|
||||
val acceptingAnswers: Boolean? = false,
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_timestamp", nullable = false, unique = false)
|
||||
val createdTimestamp: Instant,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
val id: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package at.eisibaer.jbear2.model
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.util.UUID
|
||||
|
||||
@Entity
|
||||
@Table(name = "image_files", indexes = [
|
||||
Index(name = "fk_owner_to_image_file", columnList = "fk_owned_by")
|
||||
])
|
||||
data class ImageFile (
|
||||
|
||||
@Column(name = "uuid", nullable = false, unique = true)
|
||||
val uuid: UUID,
|
||||
|
||||
@Column(name = "filename", nullable = false, unique = false)
|
||||
val filename: String,
|
||||
|
||||
@Column(name = "hash", nullable = false, unique = false)
|
||||
val hash: String,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name="fk_owned_by", referencedColumnName = "id")
|
||||
val owner: User,
|
||||
|
||||
@OneToOne
|
||||
val question: Question?,
|
||||
|
||||
@OneToOne
|
||||
val answer: Answer?,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
val id: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package at.eisibaer.jbear2.model
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "players", indexes = [
|
||||
Index(name = "fk_current_game_to_player", columnList = "fk_current_game"),
|
||||
Index(name = "fk_name_to_player", columnList = "name"),
|
||||
])
|
||||
data class Player(
|
||||
|
||||
@Column(name = "name", nullable = false, unique = false)
|
||||
val name: String,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "fk_current_game", referencedColumnName = "id")
|
||||
val currentGame: Game,
|
||||
|
||||
@Column(name = "is_allowed_to_answer", nullable = false, unique = false)
|
||||
val allowedToAnswer: Boolean? = false,
|
||||
|
||||
@Column(name = "points", nullable = false, unique = false)
|
||||
val points: Int? = 0,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
val id: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package at.eisibaer.jbear2.model
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "questions", indexes = [
|
||||
Index(name = "fk_board_entry_to_question", columnList = "fk_board_entry"),
|
||||
Index(name = "fk_image_file_to_question", columnList = "fk_image"),
|
||||
Index(name = "fk_question_type_to_question", columnList = "fk_question_type"),
|
||||
])
|
||||
data class Question(
|
||||
|
||||
@Column(name = "text", nullable = false, unique = false)
|
||||
val text: String,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "fk_question_type", referencedColumnName = "id")
|
||||
val questionType: QuestionType,
|
||||
|
||||
@OneToOne
|
||||
@JoinColumn(name = "fk_image", referencedColumnName = "id")
|
||||
val image: ImageFile?,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "fk_board_entry", referencedColumnName = "id")
|
||||
val boardEntry: BoardEntry?,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
val id: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package at.eisibaer.jbear2.model
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "question_types")
|
||||
data class QuestionType(
|
||||
|
||||
@Column(name = "title", nullable = false, unique = true)
|
||||
val title: String,
|
||||
|
||||
@Column(name = "description", nullable = false, unique = true)
|
||||
val description: String,
|
||||
|
||||
@Column(name = "active", nullable = false, unique = false)
|
||||
val active: Boolean,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
val id: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package at.eisibaer.jbear2.model
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "users", indexes = [
|
||||
Index(name = "fk_profile_picture_to_user", columnList = "fk_profile_picture"),
|
||||
Index(name = "username_to_user", columnList = "username"),
|
||||
])
|
||||
data class User(
|
||||
|
||||
@Column(name = "username", nullable = false, unique = true)
|
||||
var username: String,
|
||||
|
||||
@Column(name = "password", nullable = false, unique = false)
|
||||
var password: String,
|
||||
|
||||
@OneToMany(mappedBy = "owner")
|
||||
val boards: List<Board>,
|
||||
|
||||
@OneToOne
|
||||
@JoinColumn(name = "fk_profile_picture", referencedColumnName = "id")
|
||||
var profilePicture: ImageFile?,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
val id: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package at.eisibaer.jbear2.repository
|
||||
|
||||
import at.eisibaer.jbear2.model.User
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.util.*
|
||||
|
||||
@Repository
|
||||
interface UserRepository : JpaRepository<User, Long> {
|
||||
|
||||
fun findUserByUsername(username: String): Optional<User>;
|
||||
|
||||
fun existsByUsername(username: String): Boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package at.eisibaer.jbear2.security
|
||||
|
||||
import at.eisibaer.jbear2.security.jwt.AuthTokenFilter
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.ServletException
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
|
||||
import org.springframework.security.web.csrf.*
|
||||
import org.springframework.util.StringUtils
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
import java.io.IOException
|
||||
import java.util.function.Supplier
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
class SecurityConfiguration(
|
||||
private val userDetailService: UserDetailsService,
|
||||
private val unauthorizedHandler: AuthTokenFilter,
|
||||
) {
|
||||
|
||||
final val log: Logger = LoggerFactory.getLogger(SecurityConfiguration::class.java);
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
|
||||
return httpSecurity
|
||||
.csrf { config ->
|
||||
config
|
||||
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
||||
.csrfTokenRequestHandler(SpaCsrfTokenRequestHandler())
|
||||
}
|
||||
.addFilterAfter(CsrfCookieFilter(), BasicAuthenticationFilter::class.java)
|
||||
.authorizeHttpRequests { config ->
|
||||
config
|
||||
.requestMatchers("/api/auth/*").permitAll()
|
||||
.requestMatchers("/api/**").authenticated()
|
||||
.requestMatchers("/profile").authenticated()
|
||||
.requestMatchers("/**").permitAll()
|
||||
}
|
||||
.sessionManagement { config: SessionManagementConfigurer<HttpSecurity?> ->
|
||||
config.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
}
|
||||
.authenticationProvider(authenticationProvider())
|
||||
.addFilterBefore(unauthorizedHandler, UsernamePasswordAuthenticationFilter::class.java)
|
||||
.build()
|
||||
}
|
||||
|
||||
class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() {
|
||||
private val delegate: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
|
||||
|
||||
override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
|
||||
/*
|
||||
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
|
||||
* the CsrfToken when it is rendered in the response body.
|
||||
*/
|
||||
delegate.handle(request, response, csrfToken)
|
||||
}
|
||||
|
||||
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
|
||||
/*
|
||||
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
|
||||
* to resolve the CsrfToken. This applies when a single-page application includes
|
||||
* the header value automatically, which was obtained via a cookie containing the
|
||||
* raw CsrfToken.
|
||||
*/
|
||||
return if (StringUtils.hasText(request.getHeader(csrfToken.headerName))) {
|
||||
super.resolveCsrfTokenValue(request, csrfToken)
|
||||
} else {
|
||||
/*
|
||||
* In all other cases (e.g. if the request contains a request parameter), use
|
||||
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
|
||||
* when a server-side rendered form includes the _csrf request parameter as a
|
||||
* hidden input.
|
||||
*/
|
||||
delegate.resolveCsrfTokenValue(request, csrfToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CsrfCookieFilter : OncePerRequestFilter() {
|
||||
|
||||
@Throws(ServletException::class, IOException::class)
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
|
||||
val csrfToken = request.getAttribute("_csrf") as CsrfToken
|
||||
// Render the token value to a cookie by causing the deferred token to be loaded
|
||||
csrfToken.token
|
||||
filterChain.doFilter(request, response)
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder() : PasswordEncoder {
|
||||
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun authenticationProvider(): DaoAuthenticationProvider{
|
||||
val authProvider: DaoAuthenticationProvider = DaoAuthenticationProvider()
|
||||
|
||||
authProvider.setUserDetailsService(userDetailService)
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun authenticationManager(authConfig: AuthenticationConfiguration): AuthenticationManager{
|
||||
return authConfig.authenticationManager;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package at.eisibaer.jbear2.security.jwt
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
|
||||
class AuthEntryPointJwt: AuthenticationEntryPoint {
|
||||
|
||||
val log: Logger = LoggerFactory.getLogger(AuthEntryPointJwt::class.java);
|
||||
|
||||
override fun commence(
|
||||
request: HttpServletRequest?,
|
||||
response: HttpServletResponse?,
|
||||
authException: AuthenticationException?
|
||||
) {
|
||||
log.error("Unauthorized error: {}", authException?.message);
|
||||
response?.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package at.eisibaer.jbear2.security.jwt
|
||||
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
|
||||
@Component
|
||||
class AuthTokenFilter(
|
||||
private var jwtUtils: JwtUtils?,
|
||||
private var userDetailService: UserDetailsService?,
|
||||
): OncePerRequestFilter() {
|
||||
|
||||
val log: Logger = LoggerFactory.getLogger(AuthTokenFilter::class.java);
|
||||
|
||||
override fun doFilterInternal(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
filterChain: FilterChain
|
||||
) {
|
||||
try{
|
||||
val jwt: String? = parseJwt(request);
|
||||
if( jwt != null && jwtUtils!!.validateJwt(jwt) ){
|
||||
val username: String = jwtUtils!!.getUserNameFromJwt(jwt);
|
||||
|
||||
val userDetails: UserDetails = userDetailService!!.loadUserByUsername( username );
|
||||
|
||||
val authentication = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities);
|
||||
authentication.details = WebAuthenticationDetailsSource().buildDetails(request);
|
||||
|
||||
SecurityContextHolder.getContext().authentication = authentication;
|
||||
}
|
||||
} catch ( e: Exception ){
|
||||
log.error("Cannot set user authentication", e);
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private fun parseJwt(request: HttpServletRequest): String?{
|
||||
return jwtUtils!!.getJwtFromCookies(request);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package at.eisibaer.jbear2.security.jwt
|
||||
|
||||
import at.eisibaer.jbear2.config.ApplicationProperties
|
||||
import at.eisibaer.jbear2.security.userdetail.UserDetailsImpl
|
||||
import io.jsonwebtoken.JwtException
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.UnsupportedJwtException
|
||||
import io.jsonwebtoken.io.Decoders
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.ResponseCookie
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.util.WebUtils
|
||||
import java.util.Date
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@Component
|
||||
class JwtUtils(
|
||||
private val applicationProperties: ApplicationProperties
|
||||
) {
|
||||
|
||||
private val log: Logger = LoggerFactory.getLogger(JwtUtils::class.java);
|
||||
|
||||
fun getJwtFromCookies(request: HttpServletRequest): String?{
|
||||
return WebUtils.getCookie(request, applicationProperties.jwtCookieName)?.value;
|
||||
}
|
||||
|
||||
fun generateJwtCookie(userPrincipal: UserDetailsImpl): ResponseCookie{
|
||||
val jwt: String = generateTokenFromUsername(userPrincipal.username)
|
||||
return ResponseCookie.from(applicationProperties.jwtCookieName, jwt)
|
||||
.path("/api")
|
||||
.maxAge(applicationProperties.jwtExpirationMs/1000)
|
||||
.httpOnly(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
fun getCleanJwtCookie(): ResponseCookie {
|
||||
return ResponseCookie.from(applicationProperties.jwtCookieName, "")
|
||||
.path("/api")
|
||||
.build();
|
||||
}
|
||||
|
||||
fun getUserNameFromJwt(token: String): String{
|
||||
return Jwts.parser().verifyWith(key()).build().parseSignedClaims(token).payload.subject;
|
||||
}
|
||||
|
||||
fun key(): SecretKey{
|
||||
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(applicationProperties.jwtSecret));
|
||||
}
|
||||
|
||||
fun generateTokenFromUsername(username: String): String{
|
||||
return Jwts.builder()
|
||||
.subject(username)
|
||||
.issuedAt(Date())
|
||||
.expiration(Date(Date().time + applicationProperties.jwtExpirationMs))
|
||||
.signWith(key())
|
||||
.compact();
|
||||
}
|
||||
|
||||
fun validateJwt(authToken: String): Boolean{
|
||||
try {
|
||||
Jwts.parser().verifyWith(key()).build().parseSignedClaims(authToken);
|
||||
return true;
|
||||
} catch (e: UnsupportedJwtException) {
|
||||
log.error("UnsupportedJwtException {}", e.message);
|
||||
} catch (e: IllegalArgumentException) {
|
||||
log.error("IllegalArgumentException {}", e.message);
|
||||
} catch (e: JwtException) {
|
||||
log.error("JwtException {}", e.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package at.eisibaer.jbear2.security.userdetail
|
||||
|
||||
import at.eisibaer.jbear2.repository.UserRepository
|
||||
import at.eisibaer.jbear2.model.User
|
||||
import lombok.AllArgsConstructor
|
||||
import lombok.NoArgsConstructor
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Component
|
||||
class UserDetailServiceImpl(
|
||||
val userRepository: UserRepository,
|
||||
): UserDetailsService {
|
||||
|
||||
override fun loadUserByUsername(username: String?): UserDetailsImpl {
|
||||
val user: User = userRepository.findUserByUsername( username ?: "" ).orElseThrow { UsernameNotFoundException("User not found by username \"$username\"") }
|
||||
|
||||
return UserDetailsImpl(
|
||||
user.id!!,
|
||||
user.username,
|
||||
user.password,
|
||||
user.profilePicture?.filename,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package at.eisibaer.jbear2.security.userdetail
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import lombok.Data
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import java.util.Collections
|
||||
|
||||
@Data
|
||||
class UserDetailsImpl(
|
||||
private val id: Long,
|
||||
private val username: String,
|
||||
@JsonIgnore
|
||||
private val password: String,
|
||||
private val profilePictureFilename: String?,
|
||||
): UserDetails {
|
||||
|
||||
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
override fun getPassword(): String {
|
||||
return password;
|
||||
}
|
||||
|
||||
override fun getUsername(): String {
|
||||
return username;
|
||||
}
|
||||
|
||||
fun getProfilePictureFilename(): String {
|
||||
return profilePictureFilename ?: "";
|
||||
}
|
||||
|
||||
fun getId(): Long{
|
||||
return id;
|
||||
}
|
||||
|
||||
override fun isAccountNonExpired(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isAccountNonLocked(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isCredentialsNonExpired(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as UserDetailsImpl
|
||||
|
||||
return id == other.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
logging:
|
||||
level:
|
||||
org:
|
||||
springframework:
|
||||
security: "DEBUG"
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5499/jeobeardy?currentSchema=jeobeardy-app
|
||||
username: ${PG_USER}
|
||||
password: ${PG_PASSWORD}
|
||||
|
||||
application:
|
||||
cors-allowed-origins: [ "http://localhost:5173/" ]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5499/jeobeardy?currentSchema=jeobeardy-app
|
||||
username: ${PG_USER}
|
||||
password: ${PG_PASSWORD}
|
||||
|
||||
application:
|
||||
cors-allowed-origins: []
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
spring:
|
||||
application:
|
||||
name: "jeobeardy"
|
||||
jpa:
|
||||
show-sql: true
|
||||
hibernate:
|
||||
auto-ddl: create
|
||||
generate-ddl: true
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
default-schema: jeobeardy-app
|
||||
open-in-view: false
|
||||
|
||||
server:
|
||||
address: localhost
|
||||
port: 8008
|
||||
|
||||
application:
|
||||
cors-allowed-methods: ["GET", "POST", "DELETE", "OPTIONS"]
|
||||
jwt-cookie-name: "user_jwt"
|
||||
jwt-expiration-ms: 86400000 #1000 * 60 * 60 * 24 = 1 day
|
||||
jwt-secret: ${JWT_SECRET}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
<script type="module" crossorigin src="/assets/index-CPLpx4lq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-21nzev1V.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# ReJeobeardy
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "rejeobeardy",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"axios": "^1.7.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"@types/jsdom": "^21.1.6",
|
||||
"@types/node": "^20.12.5",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"@vue/test-utils": "^2.4.5",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"npm-run-all2": "^6.1.2",
|
||||
"sass": "^1.77.6",
|
||||
"typescript": "~5.4.0",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-vue-devtools": "^7.0.25",
|
||||
"vitest": "^1.4.0",
|
||||
"vue-tsc": "^2.0.11"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1,37 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
|
||||
import NavBar from '@/components/blocks/NavBar.vue';
|
||||
import FooterBlock from '@/components/blocks/FooterBlock.vue';
|
||||
import GenericInfoModal from '@/components/modals/GenericInfoModal.vue';
|
||||
import { provide, ref } from 'vue';
|
||||
import { infoModalShowFnKey } from './services/UtilService';
|
||||
|
||||
const infoModal = ref<InstanceType<typeof GenericInfoModal> | null>(null);
|
||||
|
||||
function showInfoModal(title: string, text: string): void{
|
||||
if( infoModal.value ){
|
||||
infoModal.value.modalTitle = title;
|
||||
infoModal.value.modalText = text;
|
||||
infoModal.value.show();
|
||||
} else {
|
||||
console.error('Modal not yet available');
|
||||
}
|
||||
}
|
||||
|
||||
provide(infoModalShowFnKey, showInfoModal);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vh-100 overflow-y-scroll overflow-x-hidden">
|
||||
<NavBar />
|
||||
|
||||
<RouterView />
|
||||
|
||||
<!-- <FooterBlock /> -->
|
||||
<GenericInfoModal
|
||||
ref="infoModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
const { VITE_API_BASE_URL, ...otherViteConfig } = import.meta.env;
|
||||
|
||||
export const ENV = {
|
||||
API_BASE_URL: VITE_API_BASE_URL as string,
|
||||
__vite__: otherViteConfig,
|
||||
};
|
||||
|
||||
console.debug(ENV);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.preserve-breaks {
|
||||
white-space: preserve-breaks;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
|
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="100mm"
|
||||
height="100mm"
|
||||
viewBox="0 0 100 100"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
sodipodi:docname="PFP_BearHead.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showguides="true"
|
||||
inkscape:zoom="0.7071068"
|
||||
inkscape:cx="48.790367"
|
||||
inkscape:cy="195.16147"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1008"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer2" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Layer 2"
|
||||
transform="translate(0.01884618,-0.22140619)">
|
||||
<ellipse
|
||||
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:4.42699;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="ellipse9"
|
||||
ry="13.127905"
|
||||
rx="11.039931"
|
||||
cy="-10.383468"
|
||||
cx="-78.467972"
|
||||
transform="matrix(-0.88593467,-0.46381003,-0.43331387,0.90124309,0,0)" />
|
||||
<ellipse
|
||||
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:4.42699;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="ellipse8"
|
||||
ry="13.127905"
|
||||
rx="11.039931"
|
||||
cy="36.007061"
|
||||
cx="11.674847"
|
||||
transform="matrix(0.88593467,-0.46381003,0.43331387,0.90124309,0,0)" />
|
||||
<circle
|
||||
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path2"
|
||||
cx="50.009468"
|
||||
cy="51.98246"
|
||||
r="34.316185" />
|
||||
<path
|
||||
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 43.246556,57.420114 c 0.75711,-2.163936 4.470201,-2.158401 6.762761,-2.158401 2.292561,0 6.005093,-0.0054 6.762762,2.158401 1.136946,3.246879 -3.319062,8.093347 -6.759248,8.094232 -3.441721,8.75e-4 -7.902893,-4.845611 -6.766275,-8.094232 z"
|
||||
id="path5"
|
||||
sodipodi:nodetypes="sssss" />
|
||||
<path
|
||||
style="display:inline;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 49.984077,65.567745 c 0,4.590286 -0.329314,5.259871 -1.868084,6.148279 -2.521632,1.455864 -5.237785,-0.51905 -6.274968,-1.11787"
|
||||
id="path6"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="display:inline;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 49.938836,65.567745 c 0,4.590286 0.329314,5.259871 1.868083,6.148279 2.521632,1.455864 5.237784,-0.51905 6.274969,-1.11787"
|
||||
id="path7"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 59.773948,42.208084 4.779484,-4.779484 4.778202,4.778204"
|
||||
id="path9" />
|
||||
<path
|
||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 29.729393,42.208084 4.779485,-4.779484 4.778201,4.778204"
|
||||
id="path10" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -0,0 +1,113 @@
|
|||
// 1. Include functions first (so you can manipulate colors, SVGs, calc, etc)
|
||||
@import "bootstrap/scss/functions";
|
||||
|
||||
// 2. Include any default variable overrides here
|
||||
|
||||
/* Colors */
|
||||
//Accents
|
||||
$cyclamen: #e86a92ff;
|
||||
$light-coral: #eb8a90ff;
|
||||
$jungle-green: #419d78ff;
|
||||
$celestial-blue: #3e92ccff;
|
||||
|
||||
$raisin-black: #161925ff;
|
||||
$delft-blue: #23395bff;
|
||||
$ucla-blue: #406e8eff;
|
||||
$powder-blue: #8ea8c3ff;
|
||||
$mint-green: #cbf7edff;
|
||||
|
||||
$space-cadet: #2b2d42ff;
|
||||
$cool-gray: #8d99aeff;
|
||||
$anti-flash-white: #edf2f4ff;
|
||||
$rich-black: #2d3543;
|
||||
|
||||
/* Variable definitions */
|
||||
$primary-accent: $cyclamen;
|
||||
$primary-accent-dark: $cyclamen;
|
||||
|
||||
$secondary-accent: $celestial-blue;
|
||||
$secondary-accent-dark: $celestial-blue;
|
||||
// $secondary-accent: $jungle-green;
|
||||
// $secondary-accent-dark: $jungle-green;
|
||||
|
||||
/* Bootstrap Colors overrides */
|
||||
$primary: $primary-accent;
|
||||
$secondary: $secondary-accent;
|
||||
// $success: $green;
|
||||
// $info: $cyan;
|
||||
// $warning: $yellow;
|
||||
// $danger: $red;
|
||||
$light: $anti-flash-white;
|
||||
$light-accented: shade-color($anti-flash-white, 10%);
|
||||
$dark: $space-cadet;
|
||||
$dark-accented: shade-color($space-cadet, 15%);
|
||||
|
||||
|
||||
/* Bootstrap Variable overrides */
|
||||
|
||||
$body-bg-dark: $space-cadet;
|
||||
$body-secondary-bg-dark: $dark-accented;
|
||||
$body-bg: $anti-flash-white;
|
||||
$body-secondary-bg: $light-accented;
|
||||
|
||||
// $font-size-base: 1.5rem;
|
||||
|
||||
// 3. Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets)
|
||||
@import "bootstrap/scss/variables";
|
||||
@import "bootstrap/scss/variables-dark";
|
||||
|
||||
|
||||
|
||||
// $navbar-dark-active-color: $primary;
|
||||
// $navbar-light-active-color: $primary;
|
||||
|
||||
/* Bootstrap Color Map adjustments */
|
||||
$custom-colors: (
|
||||
"gray": $gray-500,
|
||||
"dark-accented": $dark-accented,
|
||||
"rich-black": $rich-black,
|
||||
);
|
||||
$theme-colors: map-merge($theme-colors, $custom-colors);
|
||||
|
||||
// 4. Include any default map overrides here
|
||||
|
||||
// 5. Include remainder of required parts
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
// @import "bootstrap/scss/maps";
|
||||
// @import "bootstrap/scss/mixins";
|
||||
// @import "bootstrap/scss/root";
|
||||
|
||||
// 6. Optionally include any other parts as needed
|
||||
@import "bootstrap/scss/utilities";
|
||||
// @import "bootstrap/scss/reboot";
|
||||
// @import "bootstrap/scss/type";
|
||||
// @import "bootstrap/scss/images";
|
||||
// @import "bootstrap/scss/containers";
|
||||
// @import "bootstrap/scss/grid";
|
||||
// @import "bootstrap/scss/helpers";
|
||||
|
||||
$utilities: map-merge(
|
||||
$utilities,
|
||||
(
|
||||
"width": map-merge(
|
||||
map-get($utilities, "width"),
|
||||
(
|
||||
values: map-merge(
|
||||
map-get(map-get($utilities, "width"), "values"),
|
||||
(fit-content: fit-content),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// 7. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss`
|
||||
@import "bootstrap/scss/utilities/api";
|
||||
|
||||
// 8. Add additional custom code here
|
||||
@import "./exotic_theme.scss";
|
||||
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
|
||||
@import "../css/main.css";
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
[data-bs-theme="blue"] {
|
||||
--bs-body-color: var(--bs-pink);
|
||||
--bs-body-color-rgb: #{to-rgb($white)};
|
||||
--bs-body-bg: var(--bs-white);
|
||||
--bs-body-bg-rgb: #{to-rgb($blue)};
|
||||
--bs-tertiary-bg: #{$blue-600};
|
||||
|
||||
.dropdown-menu {
|
||||
--bs-dropdown-bg: #{mix($blue-500, $blue-600)};
|
||||
--bs-dropdown-link-active-bg: #{$blue-700};
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
--bs-btn-bg: #{mix($gray-600, $blue-400, .5)};
|
||||
--bs-btn-border-color: #{rgba($white, .25)};
|
||||
--bs-btn-hover-bg: #{darken(mix($gray-600, $blue-400, .5), 5%)};
|
||||
--bs-btn-hover-border-color: #{rgba($white, .25)};
|
||||
--bs-btn-active-bg: #{darken(mix($gray-600, $blue-400, .5), 10%)};
|
||||
--bs-btn-active-border-color: #{rgba($white, .5)};
|
||||
--bs-btn-focus-border-color: #{rgba($white, .5)};
|
||||
--bs-btn-focus-box-shadow: 0 0 0 .25rem rgba(255, 255, 255, .2);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// import { describe, it, expect } from 'vitest'
|
||||
|
||||
// import { mount } from '@vue/test-utils'
|
||||
// import HelloWorld from '../HelloWorld.vue'
|
||||
|
||||
// describe('HelloWorld', () => {
|
||||
// it('renders properly', () => {
|
||||
// const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
|
||||
// expect(wrapper.text()).toContain('Hello Vitest')
|
||||
// })
|
||||
// })
|
||||
|
||||
|
||||
// Test Example
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const boards = ref([{
|
||||
id: 1,
|
||||
boardName: "mockBoard",
|
||||
},{
|
||||
id: 2,
|
||||
boardName: "test Mock",
|
||||
},{
|
||||
id: 3,
|
||||
boardName: "Mocka Board",
|
||||
},{
|
||||
id: 4,
|
||||
boardName: "Mocka Board 2",
|
||||
}]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<template v-for="board in boards" :key="board.id">
|
||||
<div class="col-4 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{{ board.boardName }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
<button class="btn btn-sm btn-primary">
|
||||
<font-awesome-icon :icon="['fas', 'edit']" />
|
||||
Edit
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary">
|
||||
<font-awesome-icon :icon="['fas', 'play']" />
|
||||
Play
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import LocaleChanger from '@/components/blocks/LocaleChanger.vue';
|
||||
import ThemeChanger from '@/components/blocks/ThemeChanger.vue';
|
||||
import IconJeobeardy from '@/components/icons/IconJeobeardy.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-fluid bg-body-secondary p-3 position-absolute bottom-0">
|
||||
<div class="container d-flex align-content-center justify-content-around">
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<LocaleChanger />
|
||||
<ThemeChanger />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<IconJeobeardy height="2rem" width="4rem"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const localeLocalStorageKey = 'locale';
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
onMounted( () => {
|
||||
const initialLocale = localStorage.getItem( localeLocalStorageKey );
|
||||
if( initialLocale !== null ) {
|
||||
i18n.locale.value = initialLocale;
|
||||
}
|
||||
} );
|
||||
|
||||
watch(
|
||||
() => i18n.locale.value,
|
||||
( newVal ) => {
|
||||
localStorage.setItem( localeLocalStorageKey, newVal );
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ $t( `i18n.${$i18n.locale}.name` ) }}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-for=" locale in $i18n.availableLocales " :key="`locale-${locale}`" @click="$i18n.locale = locale"
|
||||
role="button">
|
||||
<a class="dropdown-item pointer" :class="[{ active: $i18n.locale === locale }]">
|
||||
{{ $t( `i18n.${locale}.name` ) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterLink, useRoute } from 'vue-router';
|
||||
|
||||
import IconJeobeardy from '@/components/icons/IconJeobeardy.vue';
|
||||
import ThemeChanger from './ThemeChanger.vue';
|
||||
import LocaleChanger from './LocaleChanger.vue';
|
||||
import { useUserStore } from '@/stores/UserStore';
|
||||
|
||||
const navNames = {
|
||||
HOME: "home",
|
||||
ABOUT: "about",
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const isActiveNav = ( navName: string ) => {
|
||||
switch( navName ){
|
||||
default:
|
||||
case navNames.HOME:
|
||||
return route.name === "home";
|
||||
case navNames.ABOUT:
|
||||
return route.name === "about";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="navbar navbar-expand-lg bg-dark-accented">
|
||||
<div class="container px-5">
|
||||
|
||||
<div class="position-absolute start-0 top-50 translate-middle-y d-flex ms-3 gap-3">
|
||||
<ThemeChanger />
|
||||
<LocaleChanger />
|
||||
</div>
|
||||
|
||||
<a class="navbar-brand rounded rounded-circle d-block d-lg-none" href="#">
|
||||
<IconJeobeardy height="3.5rem" width="3.5rem" />
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mb-2 mb-lg-0 d-flex align-items-center justify-content-center w-100">
|
||||
<li class="nav-item">
|
||||
<RouterLink to="/" class="nav-link text-light fs-3" :class="[{active: isActiveNav(navNames.HOME)}]" :aria-current="isActiveNav(navNames.HOME) ? 'page' : false">{{ $t( 'nav.home' ) }}</RouterLink>
|
||||
</li>
|
||||
<li class="nav-item px-5 mx-5 rounded-5 py-2">
|
||||
<RouterLink to="/" class="nav-link py-0">
|
||||
<IconJeobeardy height="3rem" width="4rem"/>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<RouterLink to="/about" class="nav-link text-light fs-3" :class="[{active: isActiveNav(navNames.ABOUT)}]" :aria-current="isActiveNav(navNames.HOME) ? 'page' : false">{{ $t( 'nav.about' ) }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="position-absolute end-0 top-50 translate-middle-y d-flex me-3">
|
||||
<div v-if="userStore.loggedIn">
|
||||
{{ userStore.getUserOutput }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<RouterLink to="/login" class="btn btn-sm btn-outline-primary">
|
||||
Login
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import { useBootstrapTheme } from '@/composables/colorTheme';
|
||||
|
||||
const { availableThemes, currentTheme } = useBootstrapTheme();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-changer">
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<FontAwesomeIcon :icon="currentTheme.icon" />
|
||||
{{ $t( currentTheme.name ) }}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-for="theme in availableThemes" :key="`theme-${theme.bsName}`" @click="currentTheme = theme" role="button">
|
||||
<a class="dropdown-item pointer" :class="[{active: theme.bsName === currentTheme.bsName}]">
|
||||
<FontAwesomeIcon :icon="theme.icon" />
|
||||
{{ $t(theme.name) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
interface Props {
|
||||
height?: string,
|
||||
width?: string,
|
||||
bearColor?: string,
|
||||
questionmarkColor?: string,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
bearColor: '#e86a92ff',
|
||||
questionmarkColor: '#ffffff',
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg :style="[{height: props.height}, {width: props.width}]" width="103.44236mm" height="80.726883mm" viewBox="0 0 103.44236 80.726882" version="1.1" id="svg5"
|
||||
xml:space="preserve" inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
sodipodi:docname="jeobeardy_logo_min.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs id="defs2" />
|
||||
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-51.241626,-83.781469)">
|
||||
<path
|
||||
:style="`opacity:1;fill:${props.bearColor};fill-opacity:1;stroke:${props.bearColor};stroke-width:5;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers`"
|
||||
d="m 89.597943,161.46239 c 4.957603,-11.31418 10.365454,-13.95841 18.872727,-16.46612 l -0.969,2.23271 10.17543,-3.72589 -1.44748,2.89495 c 7.77057,-2.08212 9.96019,-3.12838 17.29351,-0.22855 8.26326,-4.46932 15.33088,-3.99272 18.58862,-16.15077 0.39797,-1.48523 0.47248,-3.46705 -16.76023,-13.25582 0.6718,-1.59948 -0.64483,-6.30424 -1.44747,-7.69446 -11.87841,-11.878406 -22.82609,-9.786559 -25.14034,-11.122693 -5.10133,-2.945257 -5.77849,-9.894901 -10.741782,-10.89415 -6.64933,-1.781683 -10.639666,-0.422382 -8.015124,7.302597 -4.755054,-0.07748 -19.311199,0.225543 -19.311199,0.225543 l 4.218975,2.479364 -10.418322,0.541411 4.479459,2.526958 c -6.00567,0.93796 -10.085508,3.02646 -13.849528,6.19633 l 3.879167,3.25675 c 11.896264,-8.5256 27.407274,-7.5637 31.986403,1.85066 8.053096,14.19441 -5.364775,20.05902 -11.44594,30.07143 1.070396,5.80331 1.412146,7.38337 3.627235,11.42304 1.414891,2.45066 5.193343,11.34733 6.424889,8.53671 z"
|
||||
id="path919" sodipodi:nodetypes="sccccccccccccccccccccs" />
|
||||
<path
|
||||
:style="`opacity:1;fill:none;stroke:${props.questionmarkColor};stroke-width:8;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1`"
|
||||
d="m 57.671788,111.83817 c 4.776043,-5.45079 12.940609,-7.24498 19.118918,-7.24498 13.656487,0 13.875779,8.51413 14.475615,10.75275 0,14.09963 -13.925936,15.13618 -10.506511,27.69217 0.653123,2.43749 0.932727,3.34729 2.242618,6.17334"
|
||||
id="path2566" sodipodi:nodetypes="ccccc" />
|
||||
<ellipse
|
||||
:style="`opacity:1;fill:${props.questionmarkColor};fill-opacity:1;stroke:${props.questionmarkColor};stroke-width:2.80661;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1`"
|
||||
id="path2620" cy="182.73756" cx="-0.050881278" rx="3.2998796" ry="3.4343019" transform="rotate(-29.11515)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
import { Modal } from 'bootstrap';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const modalRef = ref<null | Element>(null);
|
||||
let modalInstance: null | Modal;
|
||||
|
||||
onMounted( () => {
|
||||
modalInstance = Modal.getOrCreateInstance(modalRef.value as Element);
|
||||
});
|
||||
|
||||
onUnmounted( () => {
|
||||
modalInstance?.dispose();
|
||||
});
|
||||
|
||||
function show(){
|
||||
if( modalInstance ){
|
||||
modalInstance.show();
|
||||
} else {
|
||||
console.error("Modal was not properly created before showing");
|
||||
}
|
||||
}
|
||||
|
||||
function hide(){
|
||||
if( modalInstance ){
|
||||
modalInstance.hide();
|
||||
} else {
|
||||
console.error("Modal was not properly created before hiding");
|
||||
}
|
||||
}
|
||||
|
||||
const modalText = ref('');
|
||||
const modalTitle = ref('');
|
||||
const modalId = ref('');
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
modalText,
|
||||
modalTitle,
|
||||
modalId,
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal" tabindex="-1" ref="modalRef" :id="modalId">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ modalTitle }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body preserve-breaks">
|
||||
<p>{{ modalText }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">{{ t("common.buttons.close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ t( 'about.whatis' ) }}
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ t( 'about.whatis' ) }}
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { userService } from '@/services/UserService';
|
||||
import IconJeobeardy from '../icons/IconJeobeardy.vue';
|
||||
|
||||
|
||||
const testResponse = ref( {} );
|
||||
|
||||
onMounted( () => {
|
||||
userService.getTest( 'ping', '42' )
|
||||
.then( ( response: any ) => {
|
||||
testResponse.value = response.data;
|
||||
} );
|
||||
} );
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="w-100 d-flex justify-content-center my-5">
|
||||
<h1>
|
||||
{{ $t( 'home.welcome' ) }}
|
||||
{{ testResponse }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="container-fluid px-0 d-flex justify-content-center align-items-center flex-column">
|
||||
<div class="row w-100 border-top">
|
||||
<div class="col-md-6 col-12 px-0 bg-body-secondary">
|
||||
<div class="d-flex justify-content-center align-items-center h-100 w-100 flex-column">
|
||||
<h3 class="m-1">
|
||||
{{ $t("join.text") }}
|
||||
</h3>
|
||||
<p>
|
||||
{{ $t("join.alreadyHostedGome") }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t("join.textCode") }}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<input type="text" class="form-control" placeholder="Code">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-primary">{{ $t("join.button") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12 px-0 mx-0">
|
||||
<img class="w-100" src="/src/assets/images/OldInGameBlurredRotated.jpeg"
|
||||
alt="Blurred, slightly tilted image of how the game looks like">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row w-100 border-bottom">
|
||||
<div class="col-md-6 col-12 px-0 mx-0">
|
||||
<img class="w-100" src="/src/assets/images/OldInGameBlurredRotated.jpeg"
|
||||
alt="Blurred, slightly tilted image of how the game looks like">
|
||||
</div>
|
||||
<div class="col-md-6 col-12 px-0 mx-0 bg-body-secondary">
|
||||
<div class="h-100 w-100 d-flex justify-content-center align-items-center flex-column">
|
||||
<h3 class="m-1">
|
||||
{{ $t("host.text") }}
|
||||
</h3>
|
||||
<p>
|
||||
{{ $t("host.alreadyHostedGome") }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t("host.textCode") }}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<input type="text" class="form-control" placeholder="Code">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-primary">{{ $t("host.button") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row w-100">
|
||||
<div class="col mt-5 mb-5">
|
||||
<div class="container mb-5 text-center">
|
||||
<h4>
|
||||
How does it work?
|
||||
</h4>
|
||||
<div class="row mt-5">
|
||||
<div class="col-4 d-flex justify-content-center align-items-center flex-column gap-3">
|
||||
<div class="border rounded-circle w-fit-content">
|
||||
<IconJeobeardy height="2rem" width="2rem" />
|
||||
</div>
|
||||
Create a Board
|
||||
</div>
|
||||
<div class="col-4 d-flex justify-content-center align-items-center flex-column gap-3">
|
||||
<div class="border rounded-circle w-fit-content">
|
||||
<IconJeobeardy height="2rem" width="2rem" />
|
||||
</div>
|
||||
Invite your friends
|
||||
</div>
|
||||
<div class="col-4 d-flex justify-content-center align-items-center flex-column gap-3">
|
||||
<div class="border rounded-circle w-fit-content">
|
||||
<IconJeobeardy height="2rem" width="2rem" />
|
||||
</div>
|
||||
Have fun playing
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-player-join-row {
|
||||
background-image: url("/src/assets/images/OldInGameBlurredRotated.jpeg");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<script setup lang="ts">
|
||||
import type { LoginDto } from '@/models/dto/LoginDto';
|
||||
import type { User } from '@/models/user/User';
|
||||
import { authService } from '@/services/AuthService';
|
||||
import { infoModalShowFnKey } from '@/services/UtilService';
|
||||
import { useUserStore } from '@/stores/UserStore';
|
||||
import useVuelidate from '@vuelidate/core';
|
||||
import { createI18nMessage, required } from '@vuelidate/validators';
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { RouterLink, useRouter } from 'vue-router';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const showInfoModal = inject(infoModalShowFnKey);
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
|
||||
const errorMessage = ref('');
|
||||
|
||||
const loginInProgress = ref(false);
|
||||
function loginUser(){
|
||||
v$.value.$touch();
|
||||
if(v$.value.$error){
|
||||
errorMessage.value = t('forms.validate-fields');
|
||||
setTimeout(() => {
|
||||
errorMessage.value = '';
|
||||
}, 3000);
|
||||
return;
|
||||
} else {
|
||||
errorMessage.value = '';
|
||||
}
|
||||
const loginDto: LoginDto = { username: username.value, password: password.value };
|
||||
loginInProgress.value = true;
|
||||
authService.loginUser( loginDto )
|
||||
.then( ( response ) => {
|
||||
userStore.setUser(response as User);
|
||||
router.push({ name: 'profile'});
|
||||
})
|
||||
.catch( ( err ) => {
|
||||
console.error(err);
|
||||
const modalText = t('login.error.process');
|
||||
if( showInfoModal !== undefined ){
|
||||
showInfoModal(t('common.error.generic'), modalText);
|
||||
} else {
|
||||
alert(modalText);
|
||||
}
|
||||
})
|
||||
.finally( () => {
|
||||
loginInProgress.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const withI18nMessage = createI18nMessage({t: t});
|
||||
const inputRequired = withI18nMessage(required);
|
||||
const rules = computed( () => ({
|
||||
username: { inputRequired },
|
||||
password: { inputRequired },
|
||||
}));
|
||||
|
||||
const v$ = useVuelidate(rules, { username, password });
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container d-flex flex-column justify-content-center align-items-center">
|
||||
<div class="row mt-5">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary p-4">
|
||||
<h2 class="mb-0">
|
||||
{{ t( 'login.loginHeader' ) }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body bg-body-secondary">
|
||||
<div class="d-flex flex-column gap-3 mt-3">
|
||||
<div class="mb-3">
|
||||
<label for="input-username">
|
||||
{{ t( 'login.username' ) }}
|
||||
<span v-if="v$.username.$error" class="text-danger ps-3">
|
||||
{{ v$.username.$errors[0].$message }}
|
||||
</span></label>
|
||||
<input v-model="username" type="text" id="input-username" class="form-control" :class="[{'border-danger': v$.username.$error}]" @blur="v$.username.$touch">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="input-username">
|
||||
{{ t( 'login.password' ) }}
|
||||
<span v-if="v$.password.$error" class="text-danger ps-3">
|
||||
{{ v$.password.$errors[0].$message }}
|
||||
</span>
|
||||
</label>
|
||||
<input v-model="password" type="password" id="input-username" class="form-control" :class="[{'border-danger': v$.password.$error}]" @blur="v$.password.$touch">
|
||||
</div>
|
||||
<div class="mb-3 d-flex justify-content-between">
|
||||
<RouterLink to="/signup" class="btn btn-outline-primary">
|
||||
{{ t( "login.signupLinkButton" ) }}
|
||||
</RouterLink>
|
||||
<button class="btn btn-primary" @click="loginUser">
|
||||
{{ t( "login.loginButton" ) }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="errorMessage" class="alert alert-danger" role="alert">{{ errorMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/stores/UserStore';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BoardSelector from '@/components/blocks/BoardSelector.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const pfpSource = computed( () => {
|
||||
return ( userStore.profilePicture === null ? "/src/assets/images/PFP_BearHead.svg" : userStore.profilePicture )
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column justify-content-center align-items-center mt-5">
|
||||
<h1>
|
||||
{{ $t('profile.yourProfile') }}
|
||||
</h1>
|
||||
<div class="ratio ratio-1x1 border rounded-5" style="width: 15rem;">
|
||||
<img :src="pfpSource" alt="Your Profile Picture" />
|
||||
</div>
|
||||
<p class="fs-3">
|
||||
{{ userStore.username }}
|
||||
</p>
|
||||
<div class="row bg-body-secondary w-100 py-4">
|
||||
<div class="col text-center">
|
||||
<h2>
|
||||
{{ $t('profile.yourBoards') }}
|
||||
</h2>
|
||||
<div class="container">
|
||||
<BoardSelector />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row w-100 py-4 mb-5">
|
||||
<div class="col text-center">
|
||||
<h2>
|
||||
{{ $t('settings.heading') }}
|
||||
</h2>
|
||||
<button class="btn btn-outline-primary">{{ $t('profile.gotoSettings') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
<script setup lang="ts">
|
||||
import { type LoginDto } from '@/models/dto/LoginDto';
|
||||
import type { User } from '@/models/user/User';
|
||||
import { authService } from '@/services/AuthService';
|
||||
import { infoModalShowFnKey } from '@/services/UtilService';
|
||||
import { useUserStore } from '@/stores/UserStore';
|
||||
import useVuelidate from '@vuelidate/core';
|
||||
import { createI18nMessage, minLength, required, sameAs } from '@vuelidate/validators';
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { RouterLink, useRouter } from 'vue-router';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const showInfoModal = inject(infoModalShowFnKey);
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const passwordRepeat = ref('');
|
||||
|
||||
const errorMessage = ref('');
|
||||
|
||||
const signupInProgress = ref(false);
|
||||
function signupUser(){
|
||||
v$.value.$touch();
|
||||
if(v$.value.$error){
|
||||
errorMessage.value = t('forms.validate-fields');
|
||||
setTimeout(() => {
|
||||
errorMessage.value = '';
|
||||
}, 3000);
|
||||
return;
|
||||
} else {
|
||||
errorMessage.value = '';
|
||||
}
|
||||
const signupDto: LoginDto = { username: username.value, password: password.value };
|
||||
signupInProgress.value = true;
|
||||
authService.signupUser( signupDto )
|
||||
.then( ( response ) => {
|
||||
userStore.setUser(response as User);
|
||||
router.push({ name: 'profile'});
|
||||
})
|
||||
.catch( ( err ) => {
|
||||
console.error(err);
|
||||
const modalText = t('signup.error.process');
|
||||
if( showInfoModal !== undefined ){
|
||||
showInfoModal(t('common.error.generic'), modalText);
|
||||
} else {
|
||||
alert(modalText);
|
||||
}
|
||||
})
|
||||
.finally( () => {
|
||||
signupInProgress.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const withI18nMessage = createI18nMessage({t: t});
|
||||
const inputRequired = withI18nMessage(required);
|
||||
const rules = computed( () => ({
|
||||
username: { inputRequired },
|
||||
password: { inputRequired, minLength: withI18nMessage(minLength(10)) },
|
||||
passwordRepeat: { inputRequired, sameAs: withI18nMessage(sameAs(password.value, t('login.password'))) },
|
||||
}));
|
||||
|
||||
const v$ = useVuelidate(rules, { username, password, passwordRepeat });
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container d-flex flex-column justify-content-center align-items-center">
|
||||
<div class="row mt-5">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary p-4">
|
||||
<h2 class="mb-0">
|
||||
{{ t( 'signup.signupHeader' ) }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body bg-body-secondary">
|
||||
<div class="d-flex flex-column gap-3 mt-3">
|
||||
<div class="mb-3">
|
||||
<label for="input-username">
|
||||
{{ t( 'login.username' ) }}
|
||||
<span v-if="v$.username.$error" class="text-danger ps-3">
|
||||
{{ v$.username.$errors[0].$message }}
|
||||
</span>
|
||||
</label>
|
||||
<input type="text" id="input-username" class="form-control" :class="[{'border-danger': v$.username.$error}]" v-model="username" @blur="v$.username.$touch">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="input-username">
|
||||
{{ t( 'login.password' ) }}
|
||||
<span v-if="v$.password.$error" class="text-danger ps-3">
|
||||
{{ v$.password.$errors[0].$message }}
|
||||
</span>
|
||||
</label>
|
||||
<input type="password" id="input-username" class="form-control" :class="[{'border-danger': v$.password.$error}]" v-model="password" @blur="v$.password.$touch">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="input-username-repeat">
|
||||
{{ t( 'signup.password-repeat' ) }}
|
||||
<span v-if="v$.passwordRepeat.$error" class="text-danger ps-3">
|
||||
{{ v$.passwordRepeat.$errors[0].$message }}
|
||||
</span>
|
||||
</label>
|
||||
<input type="password" id="input-username-repeat" class="form-control" :class="[{'border-danger': v$.passwordRepeat.$error}]" v-model="passwordRepeat" @blur="v$.passwordRepeat.$touch">
|
||||
</div>
|
||||
<div class="mb-3 d-flex justify-content-between">
|
||||
<RouterLink to="/login" class="btn btn-outline-primary">
|
||||
{{ t( "signup.loginLinkButton" ) }}
|
||||
</RouterLink>
|
||||
<button class="btn btn-primary" @click="signupUser" :disabled="signupInProgress">
|
||||
<font-awesome-icon v-if="signupInProgress" :icon="['fas', 'spinner']" spin/>
|
||||
{{ t( "signup.signupButton" ) }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="errorMessage" class="alert alert-danger" role="alert">{{ errorMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { onMounted, ref, watch } from "vue";
|
||||
|
||||
const localStorageKey = "theme";
|
||||
|
||||
const darkTheme = {
|
||||
bsName: "dark",
|
||||
name: "theme.dark.name",
|
||||
icon: ["fas", "moon"],
|
||||
};
|
||||
const lightTheme = {
|
||||
bsName: "light",
|
||||
name: "theme.light.name",
|
||||
icon: ["fas", "sun"],
|
||||
};
|
||||
const highContrastTheme = {
|
||||
bsName: "high-contrast",
|
||||
name: "theme.high-contrast.name",
|
||||
icon: ["fas", "circle-half-stroke"],
|
||||
};
|
||||
|
||||
export function useBootstrapTheme() {
|
||||
|
||||
const availableThemes = ref( [
|
||||
darkTheme,
|
||||
lightTheme,
|
||||
highContrastTheme,
|
||||
] );
|
||||
|
||||
onMounted( () => {
|
||||
let initialThemeBsName: string | null = localStorage.getItem( localStorageKey );
|
||||
if( initialThemeBsName == null ) {
|
||||
localStorage.setItem( localStorageKey, "dark" );
|
||||
initialThemeBsName = "dark";
|
||||
}
|
||||
const initialTheme = availableThemes.value.findIndex( theme => theme.bsName === initialThemeBsName );
|
||||
if( initialTheme !== -1 ) {
|
||||
currentTheme.value = availableThemes.value[initialTheme];
|
||||
}
|
||||
const htmlElem = document.getElementsByTagName( 'html' );
|
||||
htmlElem[0].dataset.bsTheme = currentTheme.value.bsName;
|
||||
} );
|
||||
|
||||
const currentTheme = ref( darkTheme );
|
||||
|
||||
watch(
|
||||
currentTheme,
|
||||
( newVal ) => {
|
||||
document.getElementsByTagName( 'html' )[0].dataset.bsTheme = newVal.bsName;
|
||||
localStorage.setItem( localStorageKey, newVal.bsName );
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
availableThemes,
|
||||
currentTheme,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"home": {
|
||||
"welcome": "Willkommen bei Jeobeardy!"
|
||||
},
|
||||
"about": {
|
||||
"whatis": "Was ist Jeobeardy?"
|
||||
},
|
||||
"login": {
|
||||
"loginHeader": "Logge dich mit deinem Jeobeardy Konto ein",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort"
|
||||
},
|
||||
"theme": {
|
||||
"dark": {
|
||||
"name": "Dunkel"
|
||||
},
|
||||
"light": {
|
||||
"name": "Hell"
|
||||
},
|
||||
"high-contrast": {
|
||||
"name": "Hoher Kontrast"
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
"en": {
|
||||
"name": "Englisch",
|
||||
"shortName": "en"
|
||||
},
|
||||
"de": {
|
||||
"name": "Deutsch",
|
||||
"shortName": "de"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"about": "Über"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"home": {
|
||||
"welcome": "Welcome to Jeobeardy!"
|
||||
},
|
||||
"about": {
|
||||
"whatis": "What is Jeobeardy?"
|
||||
},
|
||||
"login": {
|
||||
"loginHeader": "Login to your Jeobeardy Account",
|
||||
"loginButton": "Login",
|
||||
"signupLinkButton": "Sign up",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"error": {
|
||||
"process": "An error occured during the login process"
|
||||
}
|
||||
},
|
||||
"signup": {
|
||||
"signupHeader": "Create a new Jeobeardy Account",
|
||||
"signupButton": "Sign Up",
|
||||
"loginLinkButton": "Back to Login",
|
||||
"password-repeat": "Repeat Password",
|
||||
"password-not-conform": "The password does not meet the required criteria",
|
||||
"password-criteria-length": "The password needs to be at least 10 characters long",
|
||||
"error": {
|
||||
"process": "An error occured during the signup process"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"validate-fields": "Make sure that all fields are valid"
|
||||
},
|
||||
"validations": {
|
||||
"inputRequired": "This field is required",
|
||||
"minLength": "Minimum of {min} characters required",
|
||||
"sameAs": "Must match {otherName}"
|
||||
},
|
||||
"profile": {
|
||||
"yourProfile": "Your Profile",
|
||||
"yourBoards": "Your Boards",
|
||||
"gotoSettings": "Go to Settings"
|
||||
},
|
||||
"settings": {
|
||||
"heading": "Settings"
|
||||
},
|
||||
"common": {
|
||||
"buttons": {
|
||||
"close": "Close"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Error"
|
||||
}
|
||||
},
|
||||
"join": {
|
||||
"button": "Join",
|
||||
"text": "Join a Game",
|
||||
"alreadyHostedGome": "Someone else is already hosting a game?",
|
||||
"textCode": "Enter the code you get from your host and join the lobby."
|
||||
},
|
||||
"host": {
|
||||
"button": "Host",
|
||||
"text": "Host a Game",
|
||||
"alreadyHostedGome": "Wanna create a board and host a game yourself?",
|
||||
"textCode": "Wanna create a board and host a game yourself?"
|
||||
},
|
||||
"theme": {
|
||||
"dark": {
|
||||
"name": "Dark"
|
||||
},
|
||||
"light": {
|
||||
"name": "Light"
|
||||
},
|
||||
"highContrast": {
|
||||
"name": "High Contrast"
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
"en": {
|
||||
"name": "English",
|
||||
"shortName": "en"
|
||||
},
|
||||
"de": {
|
||||
"name": "German",
|
||||
"shortName": "de"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"about": "About"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
import '@/assets/scss/customized_bootstrap.scss';
|
||||
|
||||
// Importing bootstrap components which rely on js here
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { Dropdown } from 'bootstrap';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faSun, faMoon, faCircleHalfStroke, faEdit, faPlay, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import enMessages from './locales/en.json';
|
||||
import deMessages from './locales/de.json';
|
||||
|
||||
const i18n = createI18n( {
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
en: enMessages,
|
||||
de: deMessages,
|
||||
}
|
||||
} );
|
||||
|
||||
library.add(
|
||||
faSun,
|
||||
faMoon,
|
||||
faCircleHalfStroke,
|
||||
faEdit,
|
||||
faPlay,
|
||||
faSpinner
|
||||
)
|
||||
|
||||
const app = createApp( App );
|
||||
|
||||
app.use( createPinia() );
|
||||
app.component( 'FontAwesomeIcon', FontAwesomeIcon );
|
||||
app.use( router );
|
||||
app.use( i18n );
|
||||
|
||||
app.mount( '#app' );
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// export class LoginDto{
|
||||
// username: String;
|
||||
// password: String;
|
||||
|
||||
// constructor(username: String, password: String){
|
||||
// this.username = username;
|
||||
// this.password = password;
|
||||
// }
|
||||
// }
|
||||
|
||||
export type LoginDto = {
|
||||
username: String;
|
||||
password: String;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export type User = {
|
||||
username: string,
|
||||
password: string,
|
||||
profilePictureFilename: string | undefined,
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import HomePage from '@/components/pages/HomePage.vue';
|
||||
import AboutPage from '@/components/pages/AboutPage.vue';
|
||||
import LoginPage from '@/components/pages/LoginPage.vue';
|
||||
import SignupPage from '@/components/pages/SignupPage.vue';
|
||||
import GamePage from '@/components/pages/GamePage.vue';
|
||||
import ProfilePage from '@/components/pages/ProfilePage.vue';
|
||||
|
||||
const router = createRouter( {
|
||||
history: createWebHistory( import.meta.env.BASE_URL ),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomePage,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: AboutPage,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: LoginPage,
|
||||
},
|
||||
{
|
||||
path: '/signup',
|
||||
name: 'signup',
|
||||
component: SignupPage,
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
component: ProfilePage,
|
||||
},
|
||||
{
|
||||
path: '/game',
|
||||
name: 'Game',
|
||||
component: GamePage,
|
||||
},
|
||||
// {
|
||||
// path: '/about',
|
||||
// name: 'about',
|
||||
// // route level code-splitting
|
||||
// // this generates a separate chunk (About.[hash].js) for this route
|
||||
// // which is lazy-loaded when the route is visited.
|
||||
// component: () => import('../views/AboutView.vue')
|
||||
// }
|
||||
]
|
||||
} );
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { ENV } from "@/Env";
|
||||
import type { LoginDto } from '@/models/dto/LoginDto';
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
class AuthService {
|
||||
|
||||
signupUser( signupDto: LoginDto ) {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
axios.post( `${ENV.API_BASE_URL}/auth/signup`,
|
||||
signupDto,
|
||||
{
|
||||
withCredentials: true,
|
||||
},
|
||||
)
|
||||
.then( ( response ) => {
|
||||
resolve( response.data );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
loginUser( loginDto: LoginDto ) {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
axios.post( `${ENV.API_BASE_URL}/auth/login`,
|
||||
loginDto,
|
||||
{
|
||||
withCredentials: true,
|
||||
},
|
||||
)
|
||||
.then( ( response ) => {
|
||||
resolve( response );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
logoutUser() {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
axios.post( `${ENV.API_BASE_URL}/auth/logout`,
|
||||
null,
|
||||
{
|
||||
withCredentials: true,
|
||||
},
|
||||
)
|
||||
.then( ( response ) => {
|
||||
resolve( response );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const authService = new AuthService();
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { ENV } from "@/Env";
|
||||
import axios from "axios";
|
||||
|
||||
class UserService {
|
||||
|
||||
getTest( pathVar: String, param: String ) {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
axios.get( `${ENV.API_BASE_URL}/user/test/${pathVar}`,
|
||||
{
|
||||
params: {
|
||||
"param1": param,
|
||||
},
|
||||
withCredentials: true,
|
||||
},
|
||||
)
|
||||
.then( ( response ) => {
|
||||
resolve( response );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
export const userService = new UserService();
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import type { InjectionKey } from 'vue';
|
||||
|
||||
class UtilService{
|
||||
|
||||
|
||||
}
|
||||
|
||||
export const infoModalShowFnKey = Symbol() as InjectionKey<Function>;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { computed, ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import type { User } from '@/models/user/User';
|
||||
|
||||
export const useUserStore = defineStore( 'user', () => {
|
||||
const username = ref( '' );
|
||||
const profilePicture = ref<undefined | string>( undefined );
|
||||
const loggedIn = ref(false);
|
||||
|
||||
const getUserOutput = computed(() => `${username.value}`);
|
||||
|
||||
function setUser(user: User){
|
||||
username.value = user.username;
|
||||
profilePicture.value = user.profilePictureFilename;
|
||||
loggedIn.value = true;
|
||||
sessionStorage.setItem() //???
|
||||
}
|
||||
|
||||
function unsetUser(){
|
||||
username.value = '';
|
||||
profilePicture.value = undefined;
|
||||
loggedIn.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
//Refs
|
||||
username,
|
||||
profilePicture,
|
||||
loggedIn,
|
||||
|
||||
//Getters
|
||||
getUserOutput,
|
||||
|
||||
//Functions
|
||||
setUser,
|
||||
unsetUser,
|
||||
};
|
||||
} );
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/**/*", "src/**/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Node",
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
VueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
build:{
|
||||
outDir: '../resources/static',
|
||||
emptyOutDir: true,
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||
root: fileURLToPath(new URL('./', import.meta.url))
|
||||
}
|
||||
})
|
||||
)
|
||||
Loading…
Reference in New Issue