├── .github └── FUNDING.yml ├── .gitignore ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── package.json ├── settings.gradle ├── src ├── main │ ├── java │ │ └── letscode │ │ │ └── sarafan │ │ │ ├── SarafanApplication.java │ │ │ ├── config │ │ │ ├── LoggingConfig.java │ │ │ ├── WebMvcConfig.java │ │ │ ├── WebSecurityConfig.java │ │ │ └── WebSocketConfig.java │ │ │ ├── controller │ │ │ ├── CommentController.java │ │ │ ├── MainController.java │ │ │ ├── MessageController.java │ │ │ └── ProfileController.java │ │ │ ├── domain │ │ │ ├── Comment.java │ │ │ ├── Message.java │ │ │ ├── User.java │ │ │ ├── UserSubscription.java │ │ │ ├── UserSubscriptionId.java │ │ │ └── Views.java │ │ │ ├── dto │ │ │ ├── EventType.java │ │ │ ├── MessagePageDto.java │ │ │ ├── MetaDto.java │ │ │ ├── ObjectType.java │ │ │ └── WsEventDto.java │ │ │ ├── exceptions │ │ │ └── NotFoundException.java │ │ │ ├── repo │ │ │ ├── CommentRepo.java │ │ │ ├── MessageRepo.java │ │ │ ├── UserDetailsRepo.java │ │ │ └── UserSubscriptionRepo.java │ │ │ ├── service │ │ │ ├── CommentService.java │ │ │ ├── MessageService.java │ │ │ └── ProfileService.java │ │ │ └── util │ │ │ └── WsSender.java │ └── resources │ │ ├── application.properties │ │ ├── js │ │ ├── api │ │ │ ├── comment.js │ │ │ ├── messages.js │ │ │ ├── profile.js │ │ │ └── resource.js │ │ ├── components │ │ │ ├── LazyLoader.vue │ │ │ ├── UserLink.vue │ │ │ ├── comment │ │ │ │ ├── CommentForm.vue │ │ │ │ ├── CommentItem.vue │ │ │ │ └── CommentList.vue │ │ │ ├── media │ │ │ │ ├── Media.vue │ │ │ │ └── YouTube.vue │ │ │ └── messages │ │ │ │ ├── MessageForm.vue │ │ │ │ └── MessageRow.vue │ │ ├── main.js │ │ ├── pages │ │ │ ├── App.vue │ │ │ ├── Auth.vue │ │ │ ├── MessageList.vue │ │ │ ├── Profile.vue │ │ │ └── Subscriptions.vue │ │ ├── router │ │ │ └── router.js │ │ ├── store │ │ │ └── store.js │ │ └── util │ │ │ └── ws.js │ │ ├── sentry.properties │ │ ├── session_tables.sql │ │ └── templates │ │ └── index.html └── test │ └── java │ └── letscode │ └── sarafan │ └── SarafanApplicationTests.java ├── webpack.common.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: letscodedru 2 | custom: ["https://money.yandex.ru/to/41001451675086", "https://paypal.me/letscodedru", "https://qiwi.me/letscode", "https://donate.stream/mrdru"] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | /out/ 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /build/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | 29 | node_modules 30 | /src/main/resources/static/js/ 31 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.0.3.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | plugins { 14 | id "com.moowork.node" version "1.3.1" 15 | } 16 | 17 | apply plugin: 'java' 18 | apply plugin: 'eclipse' 19 | apply plugin: 'org.springframework.boot' 20 | apply plugin: 'io.spring.dependency-management' 21 | 22 | group = 'letscode' 23 | version = '0.0.1-SNAPSHOT' 24 | sourceCompatibility = 1.8 25 | 26 | 27 | task buildFront(type: YarnTask) { 28 | args = ['build'] 29 | } 30 | 31 | yarn_install.dependsOn(yarn_cache_clean) 32 | buildFront.dependsOn(yarn_install) 33 | processResources.dependsOn(buildFront) 34 | 35 | repositories { 36 | mavenCentral() 37 | } 38 | 39 | node { 40 | download = true 41 | } 42 | 43 | dependencies { 44 | compile('org.springframework.boot:spring-boot-starter-web') 45 | runtime('org.springframework.boot:spring-boot-devtools') 46 | compile('org.springframework.boot:spring-boot-starter-data-jpa') 47 | compile('org.springframework.boot:spring-boot-starter-security') 48 | compile("org.springframework.boot:spring-boot-starter-thymeleaf") 49 | compile("org.springframework.boot:spring-boot-starter-websocket") 50 | compile('org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.0.0.RELEASE') 51 | compile('org.postgresql:postgresql') 52 | compile 'org.jsoup:jsoup:1.11.3' 53 | compile('org.projectlombok:lombok') 54 | compile 'io.sentry:sentry-spring:1.7.16' 55 | testCompile('org.springframework.boot:spring-boot-starter-test') 56 | } 57 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drucoder/sarafan/179c0e816b9250a3abcc66e019f89ddab22dacef/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 10 23:46:43 NOVT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sarafan", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:drucoder/sarafan.git", 6 | "author": "Andrey ", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "webpack-dev-server --config webpack.dev.js", 10 | "build": "webpack --config webpack.prod.js" 11 | }, 12 | "dependencies": { 13 | "@babel/polyfill": "^7.2.5", 14 | "@sentry/browser": "5.1.1", 15 | "@sentry/integrations": "^5.1.0", 16 | "@stomp/stompjs": "^5.0.0-beta.3", 17 | "sockjs-client": "^1.3.0", 18 | "vue": "^2.5.17", 19 | "vue-resource": "^1.5.1", 20 | "vue-router": "^3.0.2", 21 | "vuetify": "^1.3.5", 22 | "vuex": "^3.0.1" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.0.0", 26 | "@babel/preset-env": "^7.0.0", 27 | "babel-loader": "^8.0.2", 28 | "clean-webpack-plugin": "^2.0.2", 29 | "css-loader": "^1.0.1", 30 | "vue-loader": "^15.4.1", 31 | "vue-style-loader": "^4.1.2", 32 | "vue-template-compiler": "^2.5.17", 33 | "webpack": "^4.17.2", 34 | "webpack-cli": "^3.1.0", 35 | "webpack-dev-server": "^3.1.7", 36 | "webpack-merge": "^4.2.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'sarafan' 2 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/SarafanApplication.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan; 2 | 3 | import io.sentry.Sentry; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | public class SarafanApplication { 9 | public static void main(String[] args) { 10 | Sentry.capture("Application started"); 11 | SpringApplication.run(SarafanApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/config/LoggingConfig.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.config; 2 | 3 | import io.sentry.spring.SentryExceptionResolver; 4 | import io.sentry.spring.SentryServletContextInitializer; 5 | import org.springframework.boot.web.servlet.ServletContextInitializer; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.web.servlet.HandlerExceptionResolver; 9 | 10 | @Configuration 11 | public class LoggingConfig { 12 | @Bean 13 | public HandlerExceptionResolver sentryExceptionResolver() { 14 | return new SentryExceptionResolver(); 15 | } 16 | 17 | @Bean 18 | public ServletContextInitializer sentryServletContextInitializer() { 19 | return new SentryServletContextInitializer(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.config; 2 | 3 | import org.springframework.boot.web.server.ErrorPage; 4 | import org.springframework.boot.web.server.WebServerFactoryCustomizer; 5 | import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | 11 | @Configuration 12 | public class WebMvcConfig implements WebMvcConfigurer { 13 | @Bean 14 | public WebServerFactoryCustomizer webServerCustomizer() { 15 | return container -> { 16 | container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/")); 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.config; 2 | 3 | import letscode.sarafan.domain.User; 4 | import letscode.sarafan.repo.UserDetailsRepo; 5 | import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; 6 | import org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 12 | 13 | import java.time.LocalDateTime; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableOAuth2Sso 18 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 19 | @Override 20 | protected void configure(HttpSecurity http) throws Exception { 21 | http 22 | .antMatcher("/**") 23 | .authorizeRequests() 24 | .antMatchers("/", "/login**", "/js/**", "/error**").permitAll() 25 | .anyRequest().authenticated() 26 | .and().logout().logoutSuccessUrl("/").permitAll() 27 | .and() 28 | .csrf().disable(); 29 | } 30 | 31 | @Bean 32 | public PrincipalExtractor principalExtractor(UserDetailsRepo userDetailsRepo) { 33 | return map -> { 34 | String id = (String) map.get("sub"); 35 | 36 | User user = userDetailsRepo.findById(id).orElseGet(() -> { 37 | User newUser = new User(); 38 | 39 | newUser.setId(id); 40 | newUser.setName((String) map.get("name")); 41 | newUser.setEmail((String) map.get("email")); 42 | newUser.setGender((String) map.get("gender")); 43 | newUser.setLocale((String) map.get("locale")); 44 | newUser.setUserpic((String) map.get("picture")); 45 | 46 | return newUser; 47 | }); 48 | 49 | user.setLastVisit(LocalDateTime.now()); 50 | 51 | return userDetailsRepo.save(user); 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void configureMessageBroker(MessageBrokerRegistry config) { 15 | config.enableSimpleBroker("/topic"); 16 | config.setApplicationDestinationPrefixes("/app"); 17 | } 18 | 19 | @Override 20 | public void registerStompEndpoints(StompEndpointRegistry registry) { 21 | registry.addEndpoint("/gs-guide-websocket").withSockJS(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/controller/CommentController.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.controller; 2 | 3 | import com.fasterxml.jackson.annotation.JsonView; 4 | import letscode.sarafan.domain.Comment; 5 | import letscode.sarafan.domain.User; 6 | import letscode.sarafan.domain.Views; 7 | import letscode.sarafan.service.CommentService; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | @RestController 16 | @RequestMapping("comment") 17 | public class CommentController { 18 | private final CommentService commentService; 19 | 20 | @Autowired 21 | public CommentController(CommentService commentService) { 22 | this.commentService = commentService; 23 | } 24 | 25 | @PostMapping 26 | @JsonView(Views.FullComment.class) 27 | public Comment create( 28 | @RequestBody Comment comment, 29 | @AuthenticationPrincipal User user 30 | ) { 31 | return commentService.create(comment, user); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/controller/MainController.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.controller; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.ObjectWriter; 6 | import letscode.sarafan.domain.User; 7 | import letscode.sarafan.domain.Views; 8 | import letscode.sarafan.dto.MessagePageDto; 9 | import letscode.sarafan.repo.UserDetailsRepo; 10 | import letscode.sarafan.service.MessageService; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.data.domain.PageRequest; 14 | import org.springframework.data.domain.Sort; 15 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 16 | import org.springframework.stereotype.Controller; 17 | import org.springframework.ui.Model; 18 | import org.springframework.web.bind.annotation.GetMapping; 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | 21 | import java.util.HashMap; 22 | 23 | @Controller 24 | @RequestMapping("/") 25 | public class MainController { 26 | private final MessageService messageService; 27 | private final UserDetailsRepo userDetailsRepo; 28 | 29 | @Value("${spring.profiles.active:prod}") 30 | private String profile; 31 | private final ObjectWriter messageWriter; 32 | private final ObjectWriter profileWriter; 33 | 34 | @Autowired 35 | public MainController(MessageService messageService, UserDetailsRepo userDetailsRepo, ObjectMapper mapper) { 36 | this.messageService = messageService; 37 | this.userDetailsRepo = userDetailsRepo; 38 | 39 | ObjectMapper objectMapper = mapper 40 | .setConfig(mapper.getSerializationConfig()); 41 | 42 | this.messageWriter = objectMapper 43 | .writerWithView(Views.FullMessage.class); 44 | this.profileWriter = objectMapper 45 | .writerWithView(Views.FullProfile.class); 46 | } 47 | 48 | @GetMapping 49 | public String main( 50 | Model model, 51 | @AuthenticationPrincipal User user 52 | ) throws JsonProcessingException { 53 | HashMap data = new HashMap<>(); 54 | 55 | if (user != null) { 56 | User userFromDb = userDetailsRepo.findById(user.getId()).get(); 57 | String serializedProfile = profileWriter.writeValueAsString(userFromDb); 58 | model.addAttribute("profile", serializedProfile); 59 | 60 | Sort sort = Sort.by(Sort.Direction.DESC, "id"); 61 | PageRequest pageRequest = PageRequest.of(0, MessageController.MESSAGES_PER_PAGE, sort); 62 | MessagePageDto messagePageDto = messageService.findForUser(pageRequest, user); 63 | 64 | String messages = messageWriter.writeValueAsString(messagePageDto.getMessages()); 65 | 66 | model.addAttribute("messages", messages); 67 | data.put("currentPage", messagePageDto.getCurrentPage()); 68 | data.put("totalPages", messagePageDto.getTotalPages()); 69 | } else { 70 | model.addAttribute("messages", "[]"); 71 | model.addAttribute("profile", "null"); 72 | } 73 | 74 | model.addAttribute("frontendData", data); 75 | model.addAttribute("isDevMode", "dev".equals(profile)); 76 | 77 | return "index"; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/controller/MessageController.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.controller; 2 | 3 | import com.fasterxml.jackson.annotation.JsonView; 4 | import letscode.sarafan.domain.Message; 5 | import letscode.sarafan.domain.User; 6 | import letscode.sarafan.domain.Views; 7 | import letscode.sarafan.dto.MessagePageDto; 8 | import letscode.sarafan.service.MessageService; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.data.domain.Pageable; 11 | import org.springframework.data.domain.Sort; 12 | import org.springframework.data.web.PageableDefault; 13 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import java.io.IOException; 17 | 18 | @RestController 19 | @RequestMapping("message") 20 | public class MessageController { 21 | public static final int MESSAGES_PER_PAGE = 3; 22 | 23 | private final MessageService messageService; 24 | 25 | @Autowired 26 | public MessageController(MessageService messageService) { 27 | this.messageService = messageService; 28 | } 29 | 30 | @GetMapping 31 | @JsonView(Views.FullMessage.class) 32 | public MessagePageDto list( 33 | @AuthenticationPrincipal User user, 34 | @PageableDefault(size = MESSAGES_PER_PAGE, sort = { "id" }, direction = Sort.Direction.DESC) Pageable pageable 35 | ) { 36 | return messageService.findForUser(pageable, user); 37 | } 38 | 39 | @GetMapping("{id}") 40 | @JsonView(Views.FullMessage.class) 41 | public Message getOne(@PathVariable("id") Message message) { 42 | return message; 43 | } 44 | 45 | @PostMapping 46 | @JsonView(Views.FullMessage.class) 47 | public Message create( 48 | @RequestBody Message message, 49 | @AuthenticationPrincipal User user 50 | ) throws IOException { 51 | return messageService.create(message, user); 52 | } 53 | 54 | @PutMapping("{id}") 55 | @JsonView(Views.FullMessage.class) 56 | public Message update( 57 | @PathVariable("id") Message messageFromDb, 58 | @RequestBody Message message 59 | ) throws IOException { 60 | return messageService.update(messageFromDb, message); 61 | } 62 | 63 | @DeleteMapping("{id}") 64 | public void delete(@PathVariable("id") Message message) { 65 | messageService.delete(message); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/controller/ProfileController.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.controller; 2 | 3 | import com.fasterxml.jackson.annotation.JsonView; 4 | import letscode.sarafan.domain.User; 5 | import letscode.sarafan.domain.UserSubscription; 6 | import letscode.sarafan.domain.Views; 7 | import letscode.sarafan.service.ProfileService; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import java.util.List; 13 | 14 | @RestController 15 | @RequestMapping("profile") 16 | public class ProfileController { 17 | private final ProfileService profileService; 18 | 19 | @Autowired 20 | public ProfileController(ProfileService profileService) { 21 | this.profileService = profileService; 22 | } 23 | 24 | @GetMapping("{id}") 25 | @JsonView(Views.FullProfile.class) 26 | public User get(@PathVariable("id") User user) { 27 | return user; 28 | } 29 | 30 | @PostMapping("change-subscription/{channelId}") 31 | @JsonView(Views.FullProfile.class) 32 | public User changeSubscription( 33 | @AuthenticationPrincipal User subscriber, 34 | @PathVariable("channelId") User channel 35 | ) { 36 | if (subscriber.equals(channel)) { 37 | return channel; 38 | } else { 39 | return profileService.changeSubscription(channel, subscriber); 40 | } 41 | } 42 | 43 | @GetMapping("get-subscribers/{channelId}") 44 | @JsonView(Views.IdName.class) 45 | public List subscribers( 46 | @PathVariable("channelId") User channel 47 | ) { 48 | return profileService.getSubscribers(channel); 49 | } 50 | 51 | @PostMapping("change-status/{subscriberId}") 52 | @JsonView(Views.IdName.class) 53 | public UserSubscription changeSubscriptionStatus( 54 | @AuthenticationPrincipal User channel, 55 | @PathVariable("subscriberId") User subscriber 56 | ) { 57 | return profileService.changeSubscriptionStatus(channel, subscriber); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/domain/Comment.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonView; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import javax.persistence.*; 8 | 9 | @Entity 10 | @Table 11 | @Data 12 | @EqualsAndHashCode(of = { "id" }) 13 | public class Comment { 14 | @Id 15 | @GeneratedValue 16 | @JsonView(Views.IdName.class) 17 | private Long id; 18 | 19 | @JsonView(Views.IdName.class) 20 | private String text; 21 | 22 | @ManyToOne 23 | @JoinColumn(name = "message_id") 24 | @JsonView(Views.FullComment.class) 25 | private Message message; 26 | 27 | @ManyToOne 28 | @JoinColumn(name = "user_id", nullable = false, updatable = false) 29 | @JsonView(Views.IdName.class) 30 | private User author; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/domain/Message.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.domain; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.ToString; 7 | 8 | import javax.persistence.*; 9 | import java.time.LocalDateTime; 10 | import java.util.List; 11 | 12 | @Entity 13 | @Table 14 | @ToString(of = {"id", "text"}) 15 | @EqualsAndHashCode(of = {"id"}) 16 | @Data 17 | @JsonIdentityInfo( 18 | property = "id", 19 | generator = ObjectIdGenerators.PropertyGenerator.class 20 | ) 21 | public class Message { 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.AUTO) 24 | @JsonView(Views.Id.class) 25 | private Long id; 26 | @JsonView(Views.IdName.class) 27 | private String text; 28 | 29 | @Column(updatable = false) 30 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") 31 | @JsonView(Views.FullMessage.class) 32 | private LocalDateTime creationDate; 33 | 34 | @ManyToOne 35 | @JoinColumn(name = "user_id") 36 | @JsonView(Views.FullMessage.class) 37 | private User author; 38 | 39 | @OneToMany(mappedBy = "message", orphanRemoval = true) 40 | @JsonView(Views.FullMessage.class) 41 | private List comments; 42 | 43 | @JsonView(Views.FullMessage.class) 44 | private String link; 45 | @JsonView(Views.FullMessage.class) 46 | private String linkTitle; 47 | @JsonView(Views.FullMessage.class) 48 | private String linkDescription; 49 | @JsonView(Views.FullMessage.class) 50 | private String linkCover; 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/domain/User.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.fasterxml.jackson.annotation.JsonView; 5 | import lombok.Data; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.ToString; 8 | 9 | import javax.persistence.*; 10 | import java.time.LocalDateTime; 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | 14 | @Entity 15 | @Table(name = "usr") 16 | @Data 17 | @EqualsAndHashCode(of = { "id" }) 18 | @ToString(of = { "id", "name" }) 19 | public class User { 20 | @Id 21 | @JsonView(Views.IdName.class) 22 | private String id; 23 | @JsonView(Views.IdName.class) 24 | private String name; 25 | @JsonView(Views.IdName.class) 26 | private String userpic; 27 | private String email; 28 | @JsonView(Views.FullProfile.class) 29 | private String gender; 30 | @JsonView(Views.FullProfile.class) 31 | private String locale; 32 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") 33 | @JsonView(Views.FullProfile.class) 34 | private LocalDateTime lastVisit; 35 | 36 | @JsonView(Views.FullProfile.class) 37 | @OneToMany( 38 | mappedBy = "subscriber", 39 | orphanRemoval = true 40 | ) 41 | private Set subscriptions = new HashSet<>(); 42 | 43 | @JsonView(Views.FullProfile.class) 44 | @OneToMany( 45 | mappedBy = "channel", 46 | orphanRemoval = true, 47 | cascade = CascadeType.ALL 48 | ) 49 | private Set subscribers = new HashSet<>(); 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/domain/UserSubscription.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.domain; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.ToString; 7 | 8 | import javax.persistence.EmbeddedId; 9 | import javax.persistence.Entity; 10 | import javax.persistence.ManyToOne; 11 | import javax.persistence.MapsId; 12 | import java.io.Serializable; 13 | 14 | @Entity 15 | @Data 16 | @EqualsAndHashCode(of = "id") 17 | @ToString(of = "id") 18 | public class UserSubscription implements Serializable { 19 | @EmbeddedId 20 | @JsonIgnore 21 | private UserSubscriptionId id; 22 | 23 | @MapsId("channelId") 24 | @ManyToOne 25 | @JsonView(Views.IdName.class) 26 | @JsonIdentityReference 27 | @JsonIdentityInfo( 28 | property = "id", 29 | generator = ObjectIdGenerators.PropertyGenerator.class 30 | ) 31 | private User channel; 32 | 33 | @MapsId("subscriberId") 34 | @ManyToOne 35 | @JsonView(Views.IdName.class) 36 | @JsonIdentityReference 37 | @JsonIdentityInfo( 38 | property = "id", 39 | generator = ObjectIdGenerators.PropertyGenerator.class 40 | ) 41 | private User subscriber; 42 | 43 | @JsonView(Views.IdName.class) 44 | private boolean active; 45 | 46 | public UserSubscription(User channel, User subscriber) { 47 | this.channel = channel; 48 | this.subscriber = subscriber; 49 | this.id = new UserSubscriptionId(channel.getId(), subscriber.getId()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/domain/UserSubscriptionId.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonView; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.persistence.Embeddable; 9 | import java.io.Serializable; 10 | 11 | @Embeddable 12 | @Data 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class UserSubscriptionId implements Serializable { 16 | @JsonView(Views.Id.class) 17 | private String channelId; 18 | @JsonView(Views.Id.class) 19 | private String subscriberId; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/domain/Views.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.domain; 2 | 3 | public final class Views { 4 | public interface Id {} 5 | 6 | public interface IdName extends Id {} 7 | 8 | public interface FullComment extends IdName {} 9 | 10 | public interface FullMessage extends IdName {} 11 | 12 | public interface FullProfile extends IdName {} 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/dto/EventType.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.dto; 2 | 3 | public enum EventType { 4 | CREATE, UPDATE, REMOVE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/dto/MessagePageDto.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonView; 4 | import letscode.sarafan.domain.Message; 5 | import letscode.sarafan.domain.Views; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | import java.util.List; 11 | 12 | @Getter 13 | @Setter 14 | @AllArgsConstructor 15 | @JsonView(Views.FullMessage.class) 16 | public class MessagePageDto { 17 | private List messages; 18 | private int currentPage; 19 | private int totalPages; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/dto/MetaDto.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | @Data 7 | @AllArgsConstructor 8 | public class MetaDto { 9 | private String title; 10 | private String description; 11 | private String cover; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/dto/ObjectType.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.dto; 2 | 3 | public enum ObjectType { 4 | MESSAGE, COMMENT 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/dto/WsEventDto.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonRawValue; 4 | import com.fasterxml.jackson.annotation.JsonView; 5 | import letscode.sarafan.domain.Views; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @JsonView(Views.Id.class) 12 | public class WsEventDto { 13 | private ObjectType objectType; 14 | private EventType eventType; 15 | @JsonRawValue 16 | private String body; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/exceptions/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.NOT_FOUND) 7 | public class NotFoundException extends RuntimeException { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/repo/CommentRepo.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.repo; 2 | 3 | import letscode.sarafan.domain.Comment; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface CommentRepo extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/repo/MessageRepo.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.repo; 2 | 3 | import letscode.sarafan.domain.Message; 4 | import letscode.sarafan.domain.User; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.EntityGraph; 8 | import org.springframework.data.jpa.repository.JpaRepository; 9 | 10 | import java.util.List; 11 | 12 | public interface MessageRepo extends JpaRepository { 13 | @EntityGraph(attributePaths = { "comments" }) 14 | Page findByAuthorIn(List users, Pageable pageable); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/repo/UserDetailsRepo.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.repo; 2 | 3 | import letscode.sarafan.domain.User; 4 | import org.springframework.data.jpa.repository.EntityGraph; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.Optional; 8 | 9 | public interface UserDetailsRepo extends JpaRepository { 10 | @EntityGraph(attributePaths = { "subscriptions", "subscribers" }) 11 | Optional findById(String s); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/repo/UserSubscriptionRepo.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.repo; 2 | 3 | import letscode.sarafan.domain.User; 4 | import letscode.sarafan.domain.UserSubscription; 5 | import letscode.sarafan.domain.UserSubscriptionId; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | import java.util.List; 9 | 10 | public interface UserSubscriptionRepo extends JpaRepository { 11 | List findBySubscriber(User user); 12 | 13 | List findByChannel(User channel); 14 | 15 | UserSubscription findByChannelAndSubscriber(User channel, User subscriber); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.service; 2 | 3 | import letscode.sarafan.domain.Comment; 4 | import letscode.sarafan.domain.User; 5 | import letscode.sarafan.domain.Views; 6 | import letscode.sarafan.dto.EventType; 7 | import letscode.sarafan.dto.ObjectType; 8 | import letscode.sarafan.repo.CommentRepo; 9 | import letscode.sarafan.util.WsSender; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.function.BiConsumer; 14 | 15 | @Service 16 | public class CommentService { 17 | private final CommentRepo commentRepo; 18 | private final BiConsumer wsSender; 19 | 20 | @Autowired 21 | public CommentService(CommentRepo commentRepo, WsSender wsSender) { 22 | this.commentRepo = commentRepo; 23 | this.wsSender = wsSender.getSender(ObjectType.COMMENT, Views.FullComment.class); 24 | } 25 | 26 | public Comment create(Comment comment, User user) { 27 | comment.setAuthor(user); 28 | Comment commentFromDb = commentRepo.save(comment); 29 | 30 | wsSender.accept(EventType.CREATE, commentFromDb); 31 | 32 | return commentFromDb; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.service; 2 | 3 | import letscode.sarafan.domain.Message; 4 | import letscode.sarafan.domain.User; 5 | import letscode.sarafan.domain.UserSubscription; 6 | import letscode.sarafan.domain.Views; 7 | import letscode.sarafan.dto.EventType; 8 | import letscode.sarafan.dto.MessagePageDto; 9 | import letscode.sarafan.dto.MetaDto; 10 | import letscode.sarafan.dto.ObjectType; 11 | import letscode.sarafan.repo.MessageRepo; 12 | import letscode.sarafan.repo.UserSubscriptionRepo; 13 | import letscode.sarafan.util.WsSender; 14 | import org.jsoup.Jsoup; 15 | import org.jsoup.nodes.Document; 16 | import org.jsoup.nodes.Element; 17 | import org.jsoup.select.Elements; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.data.domain.Page; 20 | import org.springframework.data.domain.Pageable; 21 | import org.springframework.stereotype.Service; 22 | 23 | import java.io.IOException; 24 | import java.time.LocalDateTime; 25 | import java.util.List; 26 | import java.util.function.BiConsumer; 27 | import java.util.regex.Matcher; 28 | import java.util.regex.Pattern; 29 | import java.util.stream.Collectors; 30 | 31 | @Service 32 | public class MessageService { 33 | private static String URL_PATTERN = "https?:\\/\\/?[\\w\\d\\._\\-%\\/\\?=&#]+"; 34 | private static String IMAGE_PATTERN = "\\.(jpeg|jpg|gif|png)$"; 35 | 36 | private static Pattern URL_REGEX = Pattern.compile(URL_PATTERN, Pattern.CASE_INSENSITIVE); 37 | private static Pattern IMG_REGEX = Pattern.compile(IMAGE_PATTERN, Pattern.CASE_INSENSITIVE); 38 | 39 | private final MessageRepo messageRepo; 40 | private final UserSubscriptionRepo userSubscriptionRepo; 41 | private final BiConsumer wsSender; 42 | 43 | @Autowired 44 | public MessageService( 45 | MessageRepo messageRepo, 46 | UserSubscriptionRepo userSubscriptionRepo, 47 | WsSender wsSender 48 | ) { 49 | this.messageRepo = messageRepo; 50 | this.userSubscriptionRepo = userSubscriptionRepo; 51 | this.wsSender = wsSender.getSender(ObjectType.MESSAGE, Views.FullMessage.class); 52 | } 53 | 54 | 55 | private void fillMeta(Message message) throws IOException { 56 | String text = message.getText(); 57 | Matcher matcher = URL_REGEX.matcher(text); 58 | 59 | if (matcher.find()) { 60 | String url = text.substring(matcher.start(), matcher.end()); 61 | 62 | matcher = IMG_REGEX.matcher(url); 63 | 64 | message.setLink(url); 65 | 66 | if (matcher.find()) { 67 | message.setLinkCover(url); 68 | } else if (!url.contains("youtu")) { 69 | MetaDto meta = getMeta(url); 70 | 71 | message.setLinkCover(meta.getCover()); 72 | message.setLinkTitle(meta.getTitle()); 73 | message.setLinkDescription(meta.getDescription()); 74 | } 75 | } 76 | } 77 | 78 | private MetaDto getMeta(String url) throws IOException { 79 | Document doc = Jsoup.connect(url).get(); 80 | 81 | Elements title = doc.select("meta[name$=title],meta[property$=title]"); 82 | Elements description = doc.select("meta[name$=description],meta[property$=description]"); 83 | Elements cover = doc.select("meta[name$=image],meta[property$=image]"); 84 | 85 | return new MetaDto( 86 | getContent(title.first()), 87 | getContent(description.first()), 88 | getContent(cover.first()) 89 | ); 90 | } 91 | 92 | private String getContent(Element element) { 93 | return element == null ? "" : element.attr("content"); 94 | } 95 | 96 | public void delete(Message message) { 97 | messageRepo.delete(message); 98 | wsSender.accept(EventType.REMOVE, message); 99 | } 100 | 101 | public Message update(Message messageFromDb, Message message) throws IOException { 102 | messageFromDb.setText(message.getText()); 103 | fillMeta(messageFromDb); 104 | Message updatedMessage = messageRepo.save(messageFromDb); 105 | 106 | wsSender.accept(EventType.UPDATE, updatedMessage); 107 | 108 | return updatedMessage; 109 | } 110 | 111 | public Message create(Message message, User user) throws IOException { 112 | message.setCreationDate(LocalDateTime.now()); 113 | fillMeta(message); 114 | message.setAuthor(user); 115 | Message updatedMessage = messageRepo.save(message); 116 | 117 | wsSender.accept(EventType.CREATE, updatedMessage); 118 | 119 | return updatedMessage; 120 | } 121 | 122 | public MessagePageDto findForUser(Pageable pageable, User user) { 123 | List channels = userSubscriptionRepo.findBySubscriber(user) 124 | .stream() 125 | .filter(UserSubscription::isActive) 126 | .map(UserSubscription::getChannel) 127 | .collect(Collectors.toList()); 128 | 129 | channels.add(user); 130 | 131 | Page page = messageRepo.findByAuthorIn(channels, pageable); 132 | 133 | return new MessagePageDto( 134 | page.getContent(), 135 | pageable.getPageNumber(), 136 | page.getTotalPages() 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/service/ProfileService.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.service; 2 | 3 | import letscode.sarafan.domain.User; 4 | import letscode.sarafan.domain.UserSubscription; 5 | import letscode.sarafan.repo.UserDetailsRepo; 6 | import letscode.sarafan.repo.UserSubscriptionRepo; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | @Service 14 | public class ProfileService { 15 | private final UserDetailsRepo userDetailsRepo; 16 | private final UserSubscriptionRepo userSubscriptionRepo; 17 | 18 | @Autowired 19 | public ProfileService( 20 | UserDetailsRepo userDetailsRepo, 21 | UserSubscriptionRepo userSubscriptionRepo 22 | ) { 23 | this.userDetailsRepo = userDetailsRepo; 24 | this.userSubscriptionRepo = userSubscriptionRepo; 25 | } 26 | 27 | public User changeSubscription(User channel, User subscriber) { 28 | List subcriptions = channel.getSubscribers() 29 | .stream() 30 | .filter(subscription -> 31 | subscription.getSubscriber().equals(subscriber) 32 | ) 33 | .collect(Collectors.toList()); 34 | 35 | if (subcriptions.isEmpty()) { 36 | UserSubscription subscription = new UserSubscription(channel, subscriber); 37 | channel.getSubscribers().add(subscription); 38 | } else { 39 | channel.getSubscribers().removeAll(subcriptions); 40 | } 41 | 42 | return userDetailsRepo.save(channel); 43 | } 44 | 45 | public List getSubscribers(User channel) { 46 | return userSubscriptionRepo.findByChannel(channel); 47 | } 48 | 49 | public UserSubscription changeSubscriptionStatus(User channel, User subscriber) { 50 | UserSubscription subscription = userSubscriptionRepo.findByChannelAndSubscriber(channel, subscriber); 51 | subscription.setActive(!subscription.isActive()); 52 | 53 | return userSubscriptionRepo.save(subscription); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/letscode/sarafan/util/WsSender.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan.util; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.ObjectWriter; 6 | import letscode.sarafan.dto.EventType; 7 | import letscode.sarafan.dto.ObjectType; 8 | import letscode.sarafan.dto.WsEventDto; 9 | import org.springframework.messaging.simp.SimpMessagingTemplate; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.function.BiConsumer; 13 | 14 | @Component 15 | public class WsSender { 16 | private final SimpMessagingTemplate template; 17 | private final ObjectMapper mapper; 18 | 19 | public WsSender(SimpMessagingTemplate template, ObjectMapper mapper) { 20 | this.template = template; 21 | this.mapper = mapper; 22 | } 23 | 24 | public BiConsumer getSender(ObjectType objectType, Class view) { 25 | ObjectWriter writer = mapper 26 | .setConfig(mapper.getSerializationConfig()) 27 | .writerWithView(view); 28 | 29 | return (EventType eventType, T payload) -> { 30 | String value = null; 31 | 32 | try { 33 | value = writer.writeValueAsString(payload); 34 | } catch (JsonProcessingException e) { 35 | throw new RuntimeException(e); 36 | } 37 | 38 | template.convertAndSend( 39 | "/topic/activity", 40 | new WsEventDto(objectType, eventType, value) 41 | ); 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost/sarafan} 2 | spring.datasource.username=${SPRING_DATASOURCE_USERNAME:postgres} 3 | spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:123} 4 | spring.jpa.generate-ddl=true 5 | 6 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 7 | 8 | security.oauth2.client.clientId=620652621050-v6a9uqrjq0ejspm5oqbek48sl6od55gt.apps.googleusercontent.com 9 | security.oauth2.client.clientSecret=${clientSecret} 10 | security.oauth2.client.clientAuthenticationScheme=form 11 | security.oauth2.client.scope=openid,email,profile 12 | security.oauth2.client.accessTokenUri=https://www.googleapis.com/oauth2/v4/token 13 | security.oauth2.client.userAuthorizationUri=https://accounts.google.com/o/oauth2/v2/auth 14 | security.oauth2.resource.userInfoUri=https://www.googleapis.com/oauth2/v3/userinfo 15 | security.oauth2.resource.preferTokenInfo=true 16 | 17 | server.port=9000 18 | 19 | spring.session.jdbc.initialize-schema=always 20 | spring.session.jdbc.table-name=SPRING_SESSION 21 | spring.session.jdbc.schema=classpath:session_tables.sql 22 | -------------------------------------------------------------------------------- /src/main/resources/js/api/comment.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const comments = Vue.resource('/comment{/id}') 4 | 5 | export default { 6 | add: comment => comments.save({}, comment), 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/js/api/messages.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const messages = Vue.resource('/message{/id}') 4 | 5 | export default { 6 | add: message => messages.save({}, message), 7 | update: message => messages.update({id: message.id}, message), 8 | remove: id => messages.remove({id}), 9 | page: page => Vue.http.get('/message', {params: { page }}) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/js/api/profile.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const profile = Vue.resource('/profile{/id}') 4 | 5 | export default { 6 | get: id => profile.get({id}), 7 | changeSubscription: channelId => Vue.http.post(`/profile/change-subscription/${channelId}`), 8 | subscriberList: channelId => Vue.http.get(`/profile/get-subscribers/${channelId}`), 9 | changeSubscriptionStatus: subscriberId => Vue.http.post(`/profile/change-status/${subscriberId}`) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/js/api/resource.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueResource from 'vue-resource' 3 | 4 | Vue.use(VueResource) 5 | -------------------------------------------------------------------------------- /src/main/resources/js/components/LazyLoader.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /src/main/resources/js/components/UserLink.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /src/main/resources/js/components/comment/CommentForm.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 41 | 42 | 45 | -------------------------------------------------------------------------------- /src/main/resources/js/components/comment/CommentItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /src/main/resources/js/components/comment/CommentList.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 35 | 36 | 39 | -------------------------------------------------------------------------------- /src/main/resources/js/components/media/Media.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 49 | 50 | 53 | -------------------------------------------------------------------------------- /src/main/resources/js/components/media/YouTube.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 28 | -------------------------------------------------------------------------------- /src/main/resources/js/components/messages/MessageForm.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /src/main/resources/js/components/messages/MessageRow.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 46 | 47 | 50 | -------------------------------------------------------------------------------- /src/main/resources/js/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import '@babel/polyfill' 4 | import 'api/resource' 5 | import router from 'router/router' 6 | import App from 'pages/App.vue' 7 | import store from 'store/store' 8 | import { connect } from './util/ws' 9 | import 'vuetify/dist/vuetify.min.css' 10 | import * as Sentry from '@sentry/browser' 11 | import * as Integrations from '@sentry/integrations' 12 | 13 | Sentry.init({ 14 | dsn: 'https://8db0384f25e140598d11d846c2d5d83b@sentry.io/1447355', 15 | integrations: [ 16 | new Integrations.Vue({ 17 | Vue, 18 | attachProps: true, 19 | }), 20 | ], 21 | }) 22 | 23 | Sentry.configureScope(scope => 24 | scope.setUser({ 25 | id: profile && profile.id, 26 | username: profile && profile.name 27 | }) 28 | ) 29 | 30 | if (profile) { 31 | connect() 32 | } 33 | 34 | Vue.use(Vuetify) 35 | 36 | new Vue({ 37 | el: '#app', 38 | store, 39 | router, 40 | render: a => a(App) 41 | }) 42 | -------------------------------------------------------------------------------- /src/main/resources/js/pages/App.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 84 | 85 | 88 | -------------------------------------------------------------------------------- /src/main/resources/js/pages/Auth.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /src/main/resources/js/pages/MessageList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | 40 | 43 | -------------------------------------------------------------------------------- /src/main/resources/js/pages/Profile.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 90 | 91 | 97 | -------------------------------------------------------------------------------- /src/main/resources/js/pages/Subscriptions.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 61 | 62 | 65 | -------------------------------------------------------------------------------- /src/main/resources/js/router/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import MessagesList from 'pages/MessageList.vue' 4 | import Auth from 'pages/Auth.vue' 5 | import Profile from 'pages/Profile.vue' 6 | import Subscriptions from 'pages/Subscriptions.vue' 7 | 8 | Vue.use(VueRouter) 9 | 10 | const routes = [ 11 | { path: '/', component: MessagesList }, 12 | { path: '/auth', component: Auth }, 13 | { path: '/user/:id?', component: Profile }, 14 | { path: '/subscriptions/:id', component: Subscriptions }, 15 | { path: '*', component: MessagesList }, 16 | ] 17 | 18 | export default new VueRouter({ 19 | mode: 'history', 20 | routes 21 | }) 22 | -------------------------------------------------------------------------------- /src/main/resources/js/store/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import messagesApi from 'api/messages' 4 | import commentApi from 'api/comment' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Vuex.Store({ 9 | state: { 10 | messages, 11 | profile, 12 | ...frontendData 13 | }, 14 | getters: { 15 | sortedMessages: state => (state.messages || []).sort((a, b) => -(a.id - b.id)) 16 | }, 17 | mutations: { 18 | addMessageMutation(state, message) { 19 | state.messages = [ 20 | ...state.messages, 21 | message 22 | ] 23 | }, 24 | updateMessageMutation(state, message) { 25 | const updateIndex = state.messages.findIndex(item => item.id === message.id) 26 | 27 | state.messages = [ 28 | ...state.messages.slice(0, updateIndex), 29 | message, 30 | ...state.messages.slice(updateIndex + 1) 31 | ] 32 | }, 33 | removeMessageMutation(state, message) { 34 | const deletionIndex = state.messages.findIndex(item => item.id === message.id) 35 | 36 | if (deletionIndex > -1) { 37 | state.messages = [ 38 | ...state.messages.slice(0, deletionIndex), 39 | ...state.messages.slice(deletionIndex + 1) 40 | ] 41 | } 42 | }, 43 | addCommentMutation(state, comment) { 44 | const updateIndex = state.messages.findIndex(item => item.id === comment.message.id) 45 | const message = state.messages[updateIndex] 46 | 47 | if (!message.comments.find(it => it.id === comment.id)) { 48 | state.messages = [ 49 | ...state.messages.slice(0, updateIndex), 50 | { 51 | ...message, 52 | comments: [ 53 | ...message.comments, 54 | comment 55 | ] 56 | }, 57 | ...state.messages.slice(updateIndex + 1) 58 | ] 59 | } 60 | }, 61 | addMessagePageMutation(state, messages) { 62 | const targetMessages = state.messages 63 | .concat(messages) 64 | .reduce((res, val) => { 65 | res[val.id] = val 66 | return res 67 | }, {}) 68 | 69 | state.messages = Object.values(targetMessages) 70 | }, 71 | updateTotalPagesMutation(state, totalPages) { 72 | state.totalPages = totalPages 73 | }, 74 | updateCurrentPageMutation(state, currentPage) { 75 | state.currentPage = currentPage 76 | } 77 | }, 78 | actions: { 79 | async addMessageAction({commit, state}, message) { 80 | const result = await messagesApi.add(message) 81 | const data = await result.json() 82 | const index = state.messages.findIndex(item => item.id === data.id) 83 | 84 | if (index > -1) { 85 | commit('updateMessageMutation', data) 86 | } else { 87 | commit('addMessageMutation', data) 88 | } 89 | }, 90 | async updateMessageAction({commit}, message) { 91 | const result = await messagesApi.update(message) 92 | const data = await result.json() 93 | commit('updateMessageMutation', data) 94 | }, 95 | async removeMessageAction({commit}, message) { 96 | const result = await messagesApi.remove(message.id) 97 | 98 | if (result.ok) { 99 | commit('removeMessageMutation', message) 100 | } 101 | }, 102 | async addCommentAction({commit, state}, comment) { 103 | const response = await commentApi.add(comment) 104 | const data = await response.json() 105 | commit('addCommentMutation', data) 106 | }, 107 | async loadPageAction({commit, state}) { 108 | const response = await messagesApi.page(state.currentPage + 1) 109 | const data = await response.json() 110 | 111 | commit('addMessagePageMutation', data.messages) 112 | commit('updateTotalPagesMutation', data.totalPages) 113 | commit('updateCurrentPageMutation', Math.min(data.currentPage, data.totalPages - 1)) 114 | } 115 | } 116 | }) 117 | -------------------------------------------------------------------------------- /src/main/resources/js/util/ws.js: -------------------------------------------------------------------------------- 1 | import SockJS from 'sockjs-client' 2 | import { Stomp } from '@stomp/stompjs' 3 | 4 | 5 | let stompClient = null 6 | const handlers = [] 7 | 8 | export function connect() { 9 | const socket = new SockJS('/gs-guide-websocket') 10 | stompClient = Stomp.over(socket) 11 | stompClient.debug = () => {} 12 | stompClient.connect({}, frame => { 13 | stompClient.subscribe('/topic/activity', message => { 14 | handlers.forEach(handler => handler(JSON.parse(message.body))) 15 | }) 16 | }) 17 | } 18 | 19 | export function addHandler(handler) { 20 | handlers.push(handler) 21 | } 22 | 23 | export function disconnect() { 24 | if (stompClient !== null) { 25 | stompClient.disconnect() 26 | } 27 | console.log("Disconnected") 28 | } 29 | 30 | export function sendMessage(message) { 31 | stompClient.send("/app/changeMessage", {}, JSON.stringify(message)) 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/sentry.properties: -------------------------------------------------------------------------------- 1 | dsn=https://3bbd23a0c7e140e8b51fd93d149a77e0@sentry.io/1447375 2 | -------------------------------------------------------------------------------- /src/main/resources/session_tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE spring_session ( 2 | primary_id CHAR(36) NOT NULL 3 | CONSTRAINT spring_session_pk 4 | PRIMARY KEY, 5 | session_id CHAR(36) NOT NULL, 6 | creation_time BIGINT NOT NULL, 7 | last_access_time BIGINT NOT NULL, 8 | max_inactive_interval INTEGER NOT NULL, 9 | expiry_time BIGINT NOT NULL, 10 | principal_name VARCHAR(300) -- <= here was 100 11 | ); 12 | 13 | CREATE UNIQUE INDEX spring_session_ix1 14 | ON spring_session (session_id); 15 | 16 | CREATE INDEX spring_session_ix2 17 | ON spring_session (expiry_time); 18 | 19 | CREATE INDEX spring_session_ix3 20 | ON spring_session (principal_name); 21 | 22 | 23 | CREATE TABLE spring_session_attributes ( 24 | session_primary_id CHAR(36) NOT NULL 25 | CONSTRAINT spring_session_attributes_fk 26 | REFERENCES spring_session 27 | ON DELETE CASCADE, 28 | attribute_name VARCHAR(200) NOT NULL, 29 | attribute_bytes BYTEA NOT NULL, 30 | CONSTRAINT spring_session_attributes_pk 31 | PRIMARY KEY (session_primary_id, attribute_name) 32 | ); 33 | -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sarafan 9 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/test/java/letscode/sarafan/SarafanApplicationTests.java: -------------------------------------------------------------------------------- 1 | package letscode.sarafan; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class SarafanApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 3 | 4 | module.exports = { 5 | entry: path.join(__dirname, 'src', 'main', 'resources', 'js', 'main.js'), 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.js$/, 10 | exclude: /(node_modules|bower_components)/, 11 | use: { 12 | loader: 'babel-loader', 13 | options: { 14 | presets: ['@babel/preset-env'] 15 | } 16 | } 17 | }, 18 | { 19 | test: /\.vue$/, 20 | loader: 'vue-loader' 21 | }, 22 | { 23 | test: /\.css$/, 24 | use: [ 25 | 'vue-style-loader', 26 | 'css-loader' 27 | ] 28 | } 29 | ] 30 | }, 31 | plugins: [ 32 | new VueLoaderPlugin() 33 | ], 34 | resolve: { 35 | modules: [ 36 | path.join(__dirname, 'src', 'main', 'resources', 'js'), 37 | path.join(__dirname, 'node_modules'), 38 | ], 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | devtool: 'source-map', 7 | devServer: { 8 | contentBase: './dist', 9 | compress: true, 10 | port: 8000, 11 | allowedHosts: [ 12 | 'localhost:9000' 13 | ], 14 | stats: 'errors-only', 15 | clientLogLevel: 'error', 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const common = require('./webpack.common.js'); 4 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 5 | 6 | module.exports = merge(common, { 7 | mode: 'production', 8 | plugins: [ 9 | new CleanWebpackPlugin(), 10 | ], 11 | output: { 12 | filename: 'main.js', 13 | path: path.resolve(__dirname, 'src', 'main', 'resources', 'static', 'js') 14 | } 15 | }); 16 | 17 | --------------------------------------------------------------------------------