├── .gitignore ├── .travis.yml ├── README.adoc ├── cf-deploy.sh ├── cf-undeploy.sh ├── manifest.yml ├── pom.xml ├── spring-a-gram-backend ├── pom.xml └── src │ ├── main │ ├── asciidoc │ │ └── index.adoc │ ├── java │ │ └── com │ │ │ └── greglturnquist │ │ │ └── springagram │ │ │ └── backend │ │ │ ├── BackendApplication.java │ │ │ ├── DataAdminController.java │ │ │ ├── DatabaseLoader.java │ │ │ ├── Gallery.java │ │ │ ├── GalleryRepository.java │ │ │ ├── Item.java │ │ │ ├── ItemRepository.java │ │ │ ├── OAuth2ServerConfiguration.java │ │ │ ├── Owner.java │ │ │ ├── RedisConfig.java │ │ │ ├── SecureTomcatConfiguration.java │ │ │ ├── SpringDataJpaUserDetailsService.java │ │ │ ├── SpringDataRestEventHandler.java │ │ │ ├── User.java │ │ │ ├── UserRepository.java │ │ │ ├── WebConfiguration.java │ │ │ ├── WebSecurityConfiguration.java │ │ │ └── WebSocketConfiguration.java │ └── resources │ │ ├── application.yml │ │ ├── cat.jpg │ │ ├── caterpillar.jpg │ │ ├── keystore.jks │ │ ├── rest-messages.properties │ │ ├── static │ │ ├── _pivotal.palette.scss │ │ ├── app │ │ │ ├── api.js │ │ │ ├── api │ │ │ │ ├── uriListConverter.js │ │ │ │ └── uriTemplateInterceptor.js │ │ │ ├── datagrid.jsx │ │ │ ├── hal.json │ │ │ ├── main.js │ │ │ ├── upload.jsx │ │ │ └── websocket-listener.js │ │ ├── bower.json │ │ ├── main.scss │ │ ├── package.json │ │ └── run.js │ │ └── templates │ │ └── index.html │ └── test │ ├── java │ └── com │ │ └── greglturnquist │ │ └── springagram │ │ └── backend │ │ ├── GalleryDocumentation.java │ │ └── ItemDocumentation.java │ └── resources │ └── logback.xml ├── spring-a-gram-eureka-server ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── greglturnquist │ │ └── springagram │ │ └── EurekaServiceDiscoveryApplication.java │ └── resources │ └── application.yml ├── spring-a-gram-frontend ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── greglturnquist │ │ └── springagram │ │ └── frontend │ │ ├── ApplicationController.java │ │ ├── ApplicationControllerHelper.java │ │ ├── BackendTrafficListener.java │ │ ├── FrontendApplication.java │ │ ├── Gallery.java │ │ ├── Item.java │ │ ├── OAuth2ServerConfiguration.java │ │ ├── RedisConfig.java │ │ ├── SecureTomcatConfiguration.java │ │ ├── SecurityDetailsLoader.java │ │ ├── SpringDataJpaUserDetailsService.java │ │ ├── TaskSchedulingConfiguration.java │ │ ├── User.java │ │ ├── UserRepository.java │ │ ├── WebConfiguration.java │ │ ├── WebSecurityConfiguration.java │ │ └── WebSocketConfiguration.java │ └── resources │ ├── application.yml │ ├── cat.jpg │ ├── caterpillar.jpg │ ├── keystore.jks │ ├── rest-messages.properties │ ├── static │ ├── app │ │ ├── api.js │ │ ├── api │ │ │ ├── uriListConverter.js │ │ │ └── uriTemplateInterceptor.js │ │ ├── follow.js │ │ ├── hateoasHelper.js │ │ ├── images.jsx │ │ ├── linkHelper.js │ │ ├── main.js │ │ ├── polyfill.js │ │ ├── spinner.js │ │ ├── twitter.js │ │ ├── upload.jsx │ │ └── websocket-listener.js │ ├── bower.json │ ├── main.scss │ ├── package.json │ └── run.js │ └── templates │ ├── _links.html │ ├── error.html │ ├── index.html │ ├── login.html │ └── oneImage.html ├── spring-a-gram-hystrix-dashboard ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── royclarkson │ │ └── Application.java │ └── resources │ └── application.yml ├── spring-a-gram-mongodb-fileservice ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── greglturnquist │ │ └── springagram │ │ └── fileservice │ │ └── mongodb │ │ ├── ApplicationController.java │ │ ├── DatabaseLoader.java │ │ ├── FileService.java │ │ ├── FileServiceResourceProcessor.java │ │ ├── MongoDbFileServiceApplication.java │ │ ├── RedisConfig.java │ │ ├── SecurityDetailsLoader.java │ │ ├── SpringDataMongoUserDetailsService.java │ │ ├── User.java │ │ ├── UserRepository.java │ │ └── WebSecurityConfiguration.java │ └── resources │ ├── application.yml │ ├── cat.jpg │ └── caterpillar.jpg ├── spring-a-gram-s3-fileservice ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── greglturnquist │ │ └── springagram │ │ └── fileservice │ │ └── s3 │ │ ├── ApplicationController.java │ │ ├── DatabaseLoader.java │ │ ├── FileService.java │ │ ├── FileServiceResourceProcessor.java │ │ ├── RedisConfig.java │ │ ├── S3FileServiceApplication.java │ │ ├── SecurityDetailsLoader.java │ │ ├── SpringDataJpaUserDetailsService.java │ │ ├── User.java │ │ ├── UserRepository.java │ │ └── WebSecurityConfiguration.java │ └── resources │ ├── application.yml │ ├── cat.jpg │ └── caterpillar.jpg ├── src └── main │ └── docker │ └── Dockerfile └── swagger ├── app.groovy ├── clean.js └── static ├── amazon.json └── real.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Operating System Files 2 | 3 | *.DS_Store 4 | Thumbs.db 5 | *.sw? 6 | .#* 7 | *# 8 | *~ 9 | *.sublime-* 10 | 11 | # Build Artifacts 12 | 13 | .gradle/ 14 | build/ 15 | target/ 16 | bin/ 17 | 18 | # Eclipse Project Files 19 | 20 | .classpath 21 | .project 22 | .settings/ 23 | 24 | # IntelliJ IDEA Files 25 | 26 | *.iml 27 | *.ipr 28 | *.iws 29 | *.idea 30 | 31 | # Bower Files 32 | 33 | .bower.json 34 | 35 | node 36 | bower_components 37 | node_modules 38 | *.css 39 | main.css* 40 | npm-debug.log 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | cache: 7 | directories: 8 | - $HOME/.m2 9 | 10 | branches: 11 | only: 12 | - master -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | :source-highlighter: prettify 2 | 3 | image:https://travis-ci.org/gregturn/spring-a-gram.svg["Build Status", link="https://travis-ci.org/gregturn/spring-a-gram"] 4 | 5 | NOTE: This snapshot copy is captured from SpringOne 2GX 2015 in Washingon, D.C. 6 | 7 | Spring-a-Gram is a demonstration of the Spring stack used to upload and view pictures. 8 | 9 | * http://projects.spring.io/spring-data-rest[Spring Data REST] 10 | * http://projects.spring.io/spring-data-jpa[Spring Data JPA] 11 | * http://projects.spring.io/spring-framework[Spring MVC] 12 | * http://projects.spring.io/spring-boot[Spring Boot] 13 | 14 | Spring Data REST makes it incredibly simple to define and export data store entities. What does this mean? You declare 15 | the POJOs that are stored in your database. In this case, we are using a JPA, in-memory data store. (Spring Data 16 | REST supports other data stores as well). You then define a related repository interface. Spring Data REST takes it 17 | from there to create a hypermedia-driven, RESTful interface using Spring MVC that lets you create, read, update, and 18 | delete data. 19 | 20 | This sample application demonstrations how declaring a backend aimed at storing image data can make it 21 | as simple as possible to upload pictures and then turn around and display them on a website. This opens 22 | the door to other front ends, like an iOS or Android app. 23 | 24 | If you want to run it right now, you need: 25 | 26 | * Java 8+ 27 | * Maven 3.0+ (http://maven.apache.org) 28 | * Bower 1.3+ (http://bower.io) 29 | 30 | To download and run: 31 | 32 | . `git clone git@github.com:gregturn/spring-a-gram.git` to grab a local copy (or clone your own fork). 33 | . `bower install` to pull down javascript libraries. If you get prompted about jQuery, accept the version specified in bower.json 34 | . `mvn clean spring-boot:run` 35 | 36 | NOTE: To deploy a copy to PWS, you need a Java 8 build pack. Execute `cf push spring-a-gram -p target/spring-a-gram-0.1.0.jar -b https://github.com/spring-io/java-buildpack.git` and you'll be set. 37 | 38 | This should get the app started courtesy of Spring Boot. Now you can traverse the app from different view points. 39 | 40 | * From the command line, interrogate the API with `curl localhost:8080/api`. Navigate to the other links. 41 | * From the desktop, visit http://localhost:8080 to see the Desktop version of the web experience. 42 | * From a mobile device, visit http://localhost:8080 and see a different flow, optimized for the smaller real estate. 43 | -------------------------------------------------------------------------------- /cf-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # create services 4 | cf create-service p-circuit-breaker-dashboard standard spring-a-gram-circuit-breaker-dashboard 5 | cf create-service p-service-registry standard spring-a-gram-service-registry 6 | cf create-service p-config-server standard spring-a-gram-config-server 7 | 8 | # build apps 9 | mvn clean package -f spring-a-gram-frontend/ 10 | mvn clean package -f spring-a-gram-backend/ 11 | mvn clean package -f spring-a-gram-mongodb-fileservice/ 12 | mvn clean package -f spring-a-gram-s3-fileservice/ 13 | 14 | # deploy apps via manifest.yml 15 | cf push 16 | -------------------------------------------------------------------------------- /cf-undeploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # delete apps 4 | cf delete spring-a-gram -f 5 | cf delete spring-a-gram-backend -f 6 | cf delete spring-a-gram-mongodb-fileservice -f 7 | cf delete spring-a-gram-s3-fileservice -f 8 | 9 | # delete services 10 | cf delete-service spring-a-gram-service-registry -f 11 | cf delete-service spring-a-gram-circuit-breaker-dashboard -f 12 | cf delete-service spring-a-gram-config-server -f 13 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: spring-a-gram 4 | host: spring-a-gram 5 | memory: 1GB 6 | instances: 1 7 | path: spring-a-gram-frontend/target/spring-a-gram-frontend-0.1.0.jar 8 | services: 9 | - spring-a-gram-service-registry 10 | - spring-a-gram-circuit-breaker-dashboard 11 | - spring-a-gram-config-server 12 | - spring-a-gram-redis 13 | env: 14 | SPRING_PROFILES_ACTIVE: basic,cloud,production 15 | 16 | - name: spring-a-gram-backend 17 | memory: 1GB 18 | instances: 1 19 | path: spring-a-gram-backend/target/spring-a-gram-backend-0.1.0.jar 20 | services: 21 | - spring-a-gram-service-registry 22 | - spring-a-gram-redis 23 | - spring-a-gram-mysql 24 | env: 25 | SPRING_PROFILES_ACTIVE: cloud,production 26 | 27 | - name: spring-a-gram-mongodb-fileservice 28 | memory: 1GB 29 | instances: 1 30 | path: spring-a-gram-mongodb-fileservice/target/spring-a-gram-mongodb-fileservice-0.1.0.jar 31 | services: 32 | - spring-a-gram-service-registry 33 | - spring-a-gram-mongodb 34 | - spring-a-gram-redis 35 | env: 36 | SPRING_PROFILES_ACTIVE: cloud,production 37 | 38 | - name: spring-a-gram-s3-fileservice 39 | memory: 1GB 40 | instances: 1 41 | path: spring-a-gram-s3-fileservice/target/spring-a-gram-s3-fileservice-0.1.0.jar 42 | services: 43 | - spring-a-gram-service-registry 44 | - spring-a-gram-redis 45 | env: 46 | SPRING_PROFILES_ACTIVE: cloud,production 47 | BUCKET: spring-a-gram.spring.io 48 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.greglturnquist.springagram 7 | spring-a-gram 8 | 0.1.0 9 | 10 | spring-a-gram-eureka-server 11 | spring-a-gram-hystrix-dashboard 12 | spring-a-gram-backend 13 | spring-a-gram-frontend 14 | spring-a-gram-mongodb-fileservice 15 | spring-a-gram-s3-fileservice 16 | 17 | pom 18 | 19 | 20 | io.pivotal.spring.cloud 21 | spring-cloud-services-starter-parent 22 | 1.0.0.M1 23 | 24 | 25 | 26 | 27 | 28 | org.springframework.data 29 | spring-data-releasetrain 30 | ${spring-data-releasetrain.version} 31 | pom 32 | import 33 | 34 | 35 | 36 | 37 | 38 | 1.8 39 | Gosling-RC1 40 | 2.0.7.RELEASE 41 | 0.17.0.RELEASE 42 | 1.0.1.RELEASE 43 | 1.2.0.RELEASE 44 | 2.7.2 45 | gregturn 46 | 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-actuator 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-starter-websocket 56 | 57 | 58 | org.springframework 59 | spring-messaging 60 | 61 | 62 | org.springframework.session 63 | spring-session 64 | ${spring-session.version} 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-starter-redis 69 | 70 | 71 | org.thymeleaf.extras 72 | thymeleaf-extras-springsecurity3 73 | 74 | 75 | com.h2database 76 | h2 77 | 78 | 79 | com.jayway.jsonpath 80 | json-path 81 | 82 | 83 | org.projectlombok 84 | lombok 85 | 1.16.4 86 | provided 87 | 88 | 89 | org.springframework.boot 90 | spring-boot-starter-test 91 | test 92 | 93 | 94 | org.springframework.restdocs 95 | spring-restdocs 96 | 0.1.0.BUILD-SNAPSHOT 97 | test 98 | 99 | 100 | 101 | 102 | 103 | 104 | org.springframework.boot 105 | spring-boot-maven-plugin 106 | 107 | 108 | com.spotify 109 | docker-maven-plugin 110 | 0.2.3 111 | 112 | ${docker.image.prefix}/${project.artifactId} 113 | src/main/docker 114 | 115 | 116 | / 117 | ${project.build.directory} 118 | ${project.build.finalName}.jar 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | spring-snapshots 129 | Spring Snapshots 130 | https://repo.spring.io/libs-snapshot 131 | 132 | true 133 | 134 | 135 | 136 | 137 | 138 | 139 | spring-snapshots 140 | Spring Snapshots 141 | https://repo.spring.io/libs-snapshot 142 | 143 | true 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /spring-a-gram-backend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.greglturnquist.springagram 9 | spring-a-gram 10 | 0.1.0 11 | 12 | 13 | com.greglturnquist.springagram 14 | spring-a-gram-backend 15 | 0.1.0 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-data-jpa 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-data-rest 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-thymeleaf 29 | 30 | 31 | io.pivotal.spring.cloud 32 | spring-cloud-services-starter-service-registry 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-security 37 | 38 | 39 | org.springframework.data 40 | spring-data-rest-hal-browser 41 | 42 | 43 | org.springframework.security.oauth 44 | spring-security-oauth2 45 | ${spring-security-oauth2.version} 46 | 47 | 48 | mysql 49 | mysql-connector-java 50 | 51 | 52 | 53 | 54 | 55 | 56 | com.github.eirslett 57 | frontend-maven-plugin 58 | 0.0.23 59 | 60 | src/main/resources/static 61 | 62 | 63 | 64 | install node and npm 65 | 66 | install-node-and-npm 67 | 68 | 69 | v0.10.33 70 | 1.3.8 71 | 72 | 73 | 74 | npm install 75 | 76 | npm 77 | 78 | 79 | install 80 | 81 | 82 | 83 | bower install 84 | 85 | bower 86 | 87 | generate-resources 88 | 89 | install 90 | 91 | 92 | 93 | 94 | 95 | nl.geodienstencentrum.maven 96 | sass-maven-plugin 97 | 2.5 98 | 99 | 100 | package 101 | process-resources 102 | 103 | update-stylesheets 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | ${basedir}/src/main/resources/static 112 | 113 | ${basedir}/src/main/resources/static 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | org.asciidoctor 130 | asciidoctor-maven-plugin 131 | 1.5.2 132 | 133 | 134 | generate-docs 135 | prepare-package 136 | 137 | process-asciidoc 138 | 139 | 140 | html 141 | book 142 | 143 | ${project.build.directory}/generated-snippets 144 | ${project.basedir} 145 | 146 | 147 | 148 | 149 | 150 | 151 | maven-resources-plugin 152 | 2.7 153 | 154 | 155 | copy-resources 156 | prepare-package 157 | 158 | copy-resources 159 | 160 | 161 | ${project.build.outputDirectory}/static/docs 162 | 163 | 164 | ${project.build.directory}/generated-docs 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/BackendApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.backend; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 21 | 22 | /** 23 | * @author Greg Turnquist 24 | */ 25 | @SpringBootApplication 26 | @EnableDiscoveryClient 27 | public class BackendApplication { 28 | 29 | public static void main(String[] args) { 30 | SpringApplication.run(BackendApplication.class); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/DataAdminController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.backend; 17 | 18 | import org.springframework.stereotype.Controller; 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @Controller 25 | public class DataAdminController { 26 | 27 | @RequestMapping("/") 28 | public String index() { 29 | return "index"; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/DatabaseLoader.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | 6 | import javax.annotation.PostConstruct; 7 | import javax.xml.bind.DatatypeConverter; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.annotation.Profile; 12 | import org.springframework.core.io.Resource; 13 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 14 | import org.springframework.security.core.authority.AuthorityUtils; 15 | import org.springframework.security.core.context.SecurityContextHolder; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.util.FileCopyUtils; 18 | 19 | @Service 20 | @Profile("!production") 21 | public class DatabaseLoader { 22 | 23 | private final GalleryRepository galleryRepository; 24 | private final ItemRepository itemRepository; 25 | private final UserRepository userRepository; 26 | private final ApplicationContext ctx; 27 | 28 | @Autowired 29 | public DatabaseLoader(GalleryRepository galleryRepository, ItemRepository itemRepository, 30 | UserRepository userRepository, ApplicationContext ctx) { 31 | 32 | this.galleryRepository = galleryRepository; 33 | this.itemRepository = itemRepository; 34 | this.userRepository = userRepository; 35 | this.ctx = ctx; 36 | } 37 | 38 | /** 39 | * Demo the application by pre-loading some cats (but only in a development environemnt) 40 | * 41 | * @throws IOException 42 | */ 43 | @PostConstruct 44 | public void init() throws IOException { 45 | 46 | User reacher = new User(); 47 | reacher.setName("greg"); 48 | reacher.setPassword("turnquist"); 49 | reacher.setRoles(new String[]{"ROLE_USER", "ROLE_ADMIN"}); 50 | reacher = userRepository.save(reacher); 51 | 52 | User strange = new User(); 53 | strange.setName("roy"); 54 | strange.setPassword("clarkson"); 55 | strange.setRoles(new String[]{"ROLE_USER"}); 56 | strange = userRepository.save(strange); 57 | 58 | SecurityContextHolder.clearContext(); 59 | 60 | // runAs(strange.getName(), strange.getPassword(), "ROLE_USER"); 61 | 62 | // Item cat = itemRepository.save(createItem(ctx.getResource("classpath:cat.jpg"), strange)); 63 | //itemRepository.save(createItem(ctx.getResource("classpath:cat.jpg"), strange)); 64 | 65 | runAs(reacher.getName(), reacher.getPassword(), "ROLE_USER"); 66 | 67 | // Item caterpillar = itemRepository.save(createItem(ctx.getResource("classpath:caterpillar.jpg"), reacher)); 68 | //itemRepository.save(createItem(ctx.getResource("classpath:caterpillar.jpg"), reacher)); 69 | 70 | Gallery catGallery = galleryRepository.save(new Gallery("Collection of cats")); 71 | Gallery truckGallery = galleryRepository.save(new Gallery("Collection of trucks")); 72 | 73 | // cat.setGallery(catGallery); 74 | // itemRepository.save(cat); 75 | 76 | // caterpillar.setGallery(truckGallery); 77 | // itemRepository.save(caterpillar); 78 | 79 | SecurityContextHolder.clearContext(); 80 | } 81 | 82 | void runAs(String username, String password, String... roles) { 83 | 84 | SecurityContextHolder.getContext().setAuthentication( 85 | new UsernamePasswordAuthenticationToken(username, password, AuthorityUtils.createAuthorityList(roles))); 86 | } 87 | 88 | private static Item createItem(Resource file, User user) throws IOException { 89 | 90 | ByteArrayOutputStream output = new ByteArrayOutputStream(); 91 | FileCopyUtils.copy(file.getInputStream(), output); 92 | Item item = new Item(); 93 | item.setImage("data:image/png;base64," + DatatypeConverter.printBase64Binary(output.toByteArray())); 94 | item.setUser(user); 95 | return item; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/Gallery.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import java.util.List; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.OneToMany; 10 | 11 | import lombok.Data; 12 | import lombok.ToString; 13 | 14 | @Data 15 | @Entity 16 | @ToString 17 | public class Gallery { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.AUTO) 21 | private long id; 22 | 23 | private String description; 24 | 25 | // tag::items-def[] 26 | @OneToMany(mappedBy = "gallery") 27 | private List items; 28 | // end::items-def[] 29 | 30 | protected Gallery() {} 31 | 32 | public Gallery(String description) { 33 | this.description = description; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/GalleryRepository.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import org.springframework.data.repository.PagingAndSortingRepository; 4 | 5 | public interface GalleryRepository extends PagingAndSortingRepository { 6 | } 7 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/Item.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.GenerationType; 6 | import javax.persistence.Id; 7 | import javax.persistence.Lob; 8 | import javax.persistence.ManyToOne; 9 | import javax.persistence.OneToOne; 10 | 11 | import lombok.Data; 12 | import lombok.ToString; 13 | 14 | import com.fasterxml.jackson.annotation.JsonIgnore; 15 | 16 | @Data 17 | @Entity 18 | @ToString(exclude = "gallery") 19 | public class Item { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.AUTO) 23 | private long id; 24 | 25 | @Lob 26 | private String image; 27 | 28 | // tag::gallery-def[] 29 | @ManyToOne 30 | private Gallery gallery; 31 | // end::gallery-def[] 32 | 33 | // tag::user-def[] 34 | @JsonIgnore 35 | @OneToOne 36 | private User user; 37 | // end::user-def[] 38 | 39 | /** 40 | * TODO: Lombok generated some error inside IntelliJ. Only solution was to hand write this setter. 41 | * @param user 42 | */ 43 | public void setUser(User user) { 44 | this.user = user; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/ItemRepository.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.repository.PagingAndSortingRepository; 6 | import org.springframework.data.repository.query.Param; 7 | import org.springframework.security.access.prepost.PreAuthorize; 8 | 9 | // tag::top-level[] 10 | @PreAuthorize("hasRole('ROLE_USER')") 11 | public interface ItemRepository extends PagingAndSortingRepository { 12 | // end::top-level[] 13 | 14 | List findByGalleryIsNull(); 15 | 16 | // tag::save-item[] 17 | @Override 18 | @PreAuthorize("#item?.user == null or #item?.user?.name == authentication?.name") 19 | Item save(@Param("item") Item item); 20 | // end::save-item[] 21 | 22 | @Override 23 | @PreAuthorize("#item?.user?.name == authentication?.name or hasRole('ROLE_ADMIN')") 24 | void delete(@Param("item") Item item); 25 | 26 | // tag::delete[] 27 | @Override 28 | @PreAuthorize("@itemRepository.findOne(#id)?.user?.name == authentication?.name or hasRole('ROLE_ADMIN')") 29 | void delete(@Param("id") Long id); 30 | // end::delete[] 31 | } 32 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/OAuth2ServerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Primary; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.security.authentication.AuthenticationManager; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 12 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 13 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 14 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 15 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 16 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 17 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; 18 | import org.springframework.security.oauth2.provider.token.DefaultTokenServices; 19 | import org.springframework.security.oauth2.provider.token.TokenStore; 20 | import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; 21 | 22 | @Configuration 23 | @Profile("oauth") 24 | public class OAuth2ServerConfiguration { 25 | 26 | private static final String RESOURCE_ID = "springagram"; 27 | 28 | @Configuration 29 | @EnableResourceServer 30 | @Profile("oauth") 31 | protected static class ResourceServerConfiguration extends 32 | ResourceServerConfigurerAdapter { 33 | 34 | @Override 35 | public void configure(ResourceServerSecurityConfigurer resources) { 36 | resources.resourceId(RESOURCE_ID); 37 | } 38 | 39 | @Override 40 | public void configure(HttpSecurity http) throws Exception { 41 | http 42 | .requestMatchers() 43 | .antMatchers("/api/**") 44 | .and() 45 | .authorizeRequests() 46 | .antMatchers("/api/**").access("#oauth2.hasScope('read') and hasRole('ROLE_USER')"); 47 | } 48 | 49 | } 50 | 51 | @Configuration 52 | @EnableAuthorizationServer 53 | @Profile("oauth") 54 | protected static class AuthorizationServerConfiguration extends 55 | AuthorizationServerConfigurerAdapter { 56 | 57 | private TokenStore tokenStore = new InMemoryTokenStore(); 58 | 59 | @Autowired 60 | @Qualifier("authenticationManagerBean") 61 | private AuthenticationManager authenticationManager; 62 | 63 | @Override 64 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) 65 | throws Exception { 66 | endpoints 67 | .tokenStore(this.tokenStore) 68 | .authenticationManager(this.authenticationManager); 69 | } 70 | 71 | @Override 72 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 73 | clients 74 | .inMemory() 75 | .withClient("springagramclient") 76 | .authorizedGrantTypes("password", "refresh_token") 77 | .authorities("USER") 78 | .scopes("read", "write") 79 | .resourceIds(RESOURCE_ID) 80 | .secret("123456"); 81 | } 82 | 83 | @Bean 84 | @Primary 85 | public DefaultTokenServices tokenServices() { 86 | DefaultTokenServices tokenServices = new DefaultTokenServices(); 87 | tokenServices.setSupportRefreshToken(true); 88 | tokenServices.setTokenStore(this.tokenStore); 89 | return tokenServices; 90 | } 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/Owner.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import org.springframework.data.rest.core.config.Projection; 4 | 5 | // tag::owner[] 6 | @Projection(name = "owner", types = Item.class) 7 | public interface Owner { 8 | 9 | public User getUser(); 10 | 11 | public String getImage(); 12 | 13 | } 14 | //end::owner[] 15 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/RedisConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.backend; 17 | 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.annotation.Configuration; 20 | import org.springframework.context.annotation.Profile; 21 | import org.springframework.data.redis.connection.RedisConnectionFactory; 22 | import org.springframework.data.redis.core.StringRedisTemplate; 23 | import org.springframework.session.data.redis.config.ConfigureRedisAction; 24 | 25 | /** 26 | * @author Greg Turnquist 27 | */ 28 | // tag::code[] 29 | @Configuration 30 | public class RedisConfig { 31 | 32 | @Bean 33 | public StringRedisTemplate template(RedisConnectionFactory factory) { 34 | return new StringRedisTemplate(factory); 35 | } 36 | 37 | @Bean 38 | @Profile("cloud") 39 | public static ConfigureRedisAction configureRedisAction() { 40 | return ConfigureRedisAction.NO_OP; 41 | } 42 | 43 | } 44 | // end::code[] 45 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/SecureTomcatConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import java.io.FileNotFoundException; 4 | 5 | import org.apache.catalina.connector.Connector; 6 | import org.apache.coyote.http11.Http11NioProtocol; 7 | 8 | import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; 9 | import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.context.annotation.Profile; 13 | import org.springframework.util.ResourceUtils; 14 | 15 | /** 16 | * Configure two Tomcat connectors, 8080/http and 8443/https. 17 | * By default, Spring Security will redirect 8080->8443 18 | */ 19 | @Configuration 20 | @Profile("ssl") 21 | public class SecureTomcatConfiguration { 22 | 23 | @Bean 24 | public EmbeddedServletContainerFactory servletContainer() throws FileNotFoundException { 25 | TomcatEmbeddedServletContainerFactory f = new TomcatEmbeddedServletContainerFactory(); 26 | f.addAdditionalTomcatConnectors(createSslConnector()); 27 | return f; 28 | } 29 | 30 | private Connector createSslConnector() throws FileNotFoundException { 31 | Connector connector = new Connector(Http11NioProtocol.class.getName()); 32 | Http11NioProtocol protocol = 33 | (Http11NioProtocol)connector.getProtocolHandler(); 34 | connector.setPort(8443); 35 | connector.setSecure(true); 36 | connector.setScheme("https"); 37 | protocol.setSSLEnabled(true); 38 | protocol.setKeyAlias("springagram"); 39 | protocol.setKeystorePass("password"); 40 | protocol.setKeystoreFile(ResourceUtils 41 | .getFile("src/main/resources/keystore.jks").getAbsolutePath()); 42 | protocol.setSslProtocol("TLS"); 43 | return connector; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/SpringDataJpaUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.core.authority.AuthorityUtils; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class SpringDataJpaUserDetailsService implements UserDetailsService { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(SpringDataJpaUserDetailsService.class); 17 | 18 | private UserRepository repository; 19 | 20 | @Autowired 21 | public SpringDataJpaUserDetailsService(UserRepository repository) { 22 | this.repository = repository; 23 | } 24 | 25 | @Override 26 | public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { 27 | log.info("Fetching user " + s); 28 | User user = repository.findByName(s); 29 | log.info("Transforming " + user + " into UserDetails object"); 30 | UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(), 31 | AuthorityUtils.createAuthorityList(user.getRoles())); 32 | log.info("About to return " + userDetails); 33 | return userDetails; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/SpringDataRestEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import static com.greglturnquist.springagram.backend.WebSocketConfiguration.*; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.data.redis.core.StringRedisTemplate; 10 | import org.springframework.data.rest.core.annotation.HandleAfterCreate; 11 | import org.springframework.data.rest.core.annotation.HandleAfterDelete; 12 | import org.springframework.data.rest.core.annotation.HandleAfterLinkDelete; 13 | import org.springframework.data.rest.core.annotation.HandleAfterLinkSave; 14 | import org.springframework.data.rest.core.annotation.HandleBeforeCreate; 15 | import org.springframework.data.rest.core.annotation.RepositoryEventHandler; 16 | import org.springframework.data.rest.core.config.RepositoryRestConfiguration; 17 | import org.springframework.data.rest.core.mapping.ResourceMappings; 18 | import org.springframework.hateoas.EntityLinks; 19 | import org.springframework.messaging.simp.SimpMessagingTemplate; 20 | import org.springframework.security.core.context.SecurityContextHolder; 21 | import org.springframework.stereotype.Component; 22 | 23 | /** 24 | * Because the published application context events are, in fact, synchronous, this handler is able to 25 | * hold up {@link Item} creation until the {@link User} 26 | * can be retrieved and populated. 27 | * 28 | * Since this is in the same thread of execution as the original REST call, 29 | * {@link SecurityContextHolder} can be used to retrieve the username, 30 | * and hence do the user lookup. 31 | */ 32 | // tag::event-handler-one[] 33 | @Component 34 | @RepositoryEventHandler(Item.class) 35 | public class SpringDataRestEventHandler { 36 | // end::event-handler-one[] 37 | 38 | private static final Logger log = LoggerFactory.getLogger(SpringDataRestEventHandler.class); 39 | 40 | private final UserRepository repository; 41 | private final EntityLinks entityLinks; 42 | private final ResourceMappings resourceMappings; 43 | private final RepositoryRestConfiguration config; 44 | private final StringRedisTemplate redis; 45 | private final SimpMessagingTemplate websocket; 46 | 47 | @Autowired 48 | public SpringDataRestEventHandler(UserRepository repository, StringRedisTemplate redis, EntityLinks entityLinks, 49 | ResourceMappings resourceMappings, RepositoryRestConfiguration config, 50 | SimpMessagingTemplate websocket) { 51 | 52 | this.repository = repository; 53 | this.redis = redis; 54 | this.entityLinks = entityLinks; 55 | this.resourceMappings = resourceMappings; 56 | this.config = config; 57 | this.websocket = websocket; 58 | } 59 | 60 | // tag::event-handler-two[] 61 | @HandleBeforeCreate 62 | public void applyUserInformationUsingSecurityContext(Item item) { 63 | 64 | String name = SecurityContextHolder.getContext().getAuthentication().getName(); 65 | User user = repository.findByName(name); 66 | if (user == null) { 67 | User newUser = new User(); 68 | newUser.setName(name); 69 | user = repository.save(newUser); 70 | } 71 | item.setUser(user); 72 | } 73 | // end::event-handler-two[] 74 | 75 | // tag::event-handler-three[] 76 | @HandleAfterCreate 77 | public void notifyAllClientsAboutNewItem(Item item) { 78 | 79 | log.info("Just created new item " + item); 80 | publish("backend.newItem", pathFor(item)); 81 | } 82 | 83 | @HandleAfterDelete 84 | public void notifyAllClientsAboutItemDeletion(Item item) { 85 | 86 | log.info("Just deleted item " + item); 87 | publish("backend.deleteItem", pathFor(item)); 88 | } 89 | // end::event-handler-three[] 90 | 91 | @HandleAfterLinkDelete 92 | public void notifyAllClientsWhenRemovedFromGallery(Item item, Object obj) { 93 | 94 | log.info("Item " + item + " just had an afterLinkDelete..."); 95 | log.info("Related object => " + obj); 96 | publish("backend.removeItemFromGallery-item", pathFor(item)); 97 | publish("backend.removeItemFromGallery-gallery", pathFor((Gallery) obj)); 98 | } 99 | 100 | @HandleAfterLinkSave 101 | public void notifyAllClientsWhenAddedToGallery(Item item, Object obj) { 102 | 103 | log.info("Item " + item + " just had an afterLinkSave..."); 104 | publish("backend.addItemToGallery-item", pathFor(item)); 105 | publish("backend.addItemToGallery-gallery", pathFor(item.getGallery())); 106 | } 107 | 108 | private void publish(String routingKey, String message) { 109 | 110 | redis.convertAndSend(MESSAGE_PREFIX + "/" + routingKey, message); 111 | websocket.convertAndSend(MESSAGE_PREFIX + "/" + routingKey, message); 112 | } 113 | 114 | // tag::event-handler-four[] 115 | private String pathFor(Item item) { 116 | 117 | return entityLinks.linkForSingleResource(item.getClass(), 118 | item.getId()).toUri().getPath(); 119 | } 120 | // end::event-handler-four[] 121 | 122 | private String pathFor(Gallery gallery) { 123 | 124 | return entityLinks.linkForSingleResource(gallery.getClass(), 125 | gallery.getId()).toUri().getPath(); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/User.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.Id; 8 | 9 | import lombok.Data; 10 | import lombok.ToString; 11 | 12 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | 15 | import com.fasterxml.jackson.annotation.JsonIgnore; 16 | 17 | @Data 18 | @ToString(exclude = "password") 19 | @Entity 20 | public class User implements Serializable { 21 | 22 | public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); 23 | 24 | @Id @GeneratedValue 25 | private Long id; 26 | 27 | private String name; 28 | 29 | // tag::user[] 30 | // This field MUST be protected against any form of 31 | // serialization to avoid security leakage 32 | @JsonIgnore 33 | private String password; 34 | //end::user[] 35 | 36 | private String[] roles; 37 | 38 | public void setPassword(String password) { 39 | this.password = PASSWORD_ENCODER.encode(password); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | 6 | // tag::user-repository[] 7 | @RepositoryRestResource(exported = false) 8 | public interface UserRepository extends CrudRepository { 9 | 10 | User findByName(String name); 11 | } 12 | // end::user-repository[] 13 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/WebConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 6 | 7 | @Configuration 8 | public class WebConfiguration extends WebMvcConfigurerAdapter { 9 | 10 | @Override 11 | public void addViewControllers(ViewControllerRegistry registry) { 12 | registry.addViewController("/login").setViewName("login"); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/WebSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.core.env.Environment; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 9 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 13 | import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableGlobalMethodSecurity(prePostEnabled = true) 18 | @EnableRedisHttpSession 19 | public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { 20 | 21 | @Autowired 22 | SpringDataJpaUserDetailsService userDetailsService; 23 | 24 | @Autowired 25 | Environment env; 26 | 27 | @Override 28 | public void configure(AuthenticationManagerBuilder auth) throws Exception { 29 | auth.userDetailsService(userDetailsService).passwordEncoder(User.PASSWORD_ENCODER); 30 | } 31 | 32 | // Needed by Spring Security OAuth 33 | @Override 34 | @Bean 35 | public AuthenticationManager authenticationManagerBean() throws Exception { 36 | return super.authenticationManagerBean(); 37 | } 38 | 39 | @Override 40 | protected void configure(HttpSecurity http) throws Exception { 41 | http 42 | .authorizeRequests() 43 | // NOTE: If you add other static resources to src/main/resources, they must be 44 | // listed here to avoid security checks 45 | .antMatchers("/docs/**").permitAll() 46 | .anyRequest().authenticated() 47 | .and() 48 | .httpBasic() 49 | .and() 50 | .csrf().disable(); 51 | 52 | if (env.acceptsProfiles("ssl")) { 53 | http.requiresChannel().anyRequest().requiresSecure(); 54 | } 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/java/com/greglturnquist/springagram/backend/WebSocketConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.session.ExpiringSession; 6 | import org.springframework.session.web.socket.config.annotation.AbstractSessionWebSocketMessageBrokerConfigurer; 7 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 8 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 9 | 10 | @Configuration 11 | @EnableWebSocketMessageBroker 12 | public class WebSocketConfiguration extends AbstractSessionWebSocketMessageBrokerConfigurer { 13 | 14 | public static final String MESSAGE_PREFIX = "/topic"; 15 | 16 | @Override 17 | public void configureMessageBroker(MessageBrokerRegistry registry) { 18 | registry.enableSimpleBroker(MESSAGE_PREFIX); 19 | registry.setApplicationDestinationPrefixes("/app"); 20 | } 21 | 22 | @Override 23 | protected void configureStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { 24 | stompEndpointRegistry.addEndpoint("/spring-a-gram").withSockJS(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: spring-a-gram-backend 4 | data: 5 | rest: 6 | basePath: /api 7 | 8 | server: 9 | port: 0 # Spring Boot randomly assigns a port number 10 | 11 | eureka: 12 | client: 13 | serviceUrl: 14 | defaultZone: ${eureka.address:localhost:8761}/eureka/ 15 | instance: 16 | leaseRenewalIntervalInSeconds: 5 17 | hostname: ${vcap.application.uris[0]:localhost} 18 | metadataMap: 19 | instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}} 20 | 21 | logging: 22 | level: 23 | ROOT: INFO 24 | com.gregturnquist.springagram: DEBUG 25 | org.hibernate.SQL: DEBUG 26 | 27 | --- 28 | spring: 29 | profiles: cloud 30 | 31 | eureka: 32 | instance: 33 | nonSecurePort: 80 34 | 35 | --- 36 | spring: 37 | profiles: production 38 | jpa: 39 | database: mysql 40 | hibernate: 41 | ddl-auto: update 42 | datasource: 43 | driverClassName: com.mysql.jdbc.Driver 44 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregturn/spring-a-gram/171f8675ed22308936e43eebe4f18ef6d90d9010/spring-a-gram-backend/src/main/resources/cat.jpg -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/caterpillar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregturn/spring-a-gram/171f8675ed22308936e43eebe4f18ef6d90d9010/spring-a-gram-backend/src/main/resources/caterpillar.jpg -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregturn/spring-a-gram/171f8675ed22308936e43eebe4f18ef6d90d9010/spring-a-gram-backend/src/main/resources/keystore.jks -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/rest-messages.properties: -------------------------------------------------------------------------------- 1 | rest.description.item=An image 2 | rest.description.item.id=primary key used internally to store an item (not for RESTful usage) 3 | rest.description.item.image=An Base64-encoded version of the image 4 | rest.description.item.gallery=If populated, the gallery the image is linked to 5 | rest.description.item.htmlUrl=The URL to view the image on a web page (instead of the HATEOAS record) 6 | 7 | rest.description.gallery=A collection of images 8 | rest.description.gallery.id=primary key used internally to store a gallery (not for RESTful usage) 9 | rest.description.gallery.description=Description of the entire gallery 10 | rest.description.gallery.items=Links to images associated with this gallery 11 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/_pivotal.palette.scss: -------------------------------------------------------------------------------- 1 | // Accent colors - teal for now 2 | // ------------------------- 3 | 4 | $teal-1: #025a53; 5 | $teal-2: #00776d; 6 | $teal-3: #00a79d; 7 | $teal-4: #92d0c0; 8 | $teal-5: #71ffda; 9 | 10 | 11 | // Grays - use these rather than the bootstrap grays 12 | // ------------------------- 13 | 14 | $gray-1: #282828; 15 | $gray-2: #424242; 16 | $gray-3: #686868; 17 | $gray-4: #8d8e8e; 18 | $gray-5: #b4b4b4; 19 | $gray-6: #c3c5c7; 20 | $gray-7: #d4d9d9; 21 | $gray-8: #e0e4e5; 22 | $gray-9: #ecefef; 23 | $gray-10: #F8F8F8; 24 | $gray-11: white; 25 | 26 | // Shadows 27 | // ------------------------- 28 | 29 | $shadow-1 : rgba(0, 0, 0, 0.3); 30 | $shadow-2 : rgba(0, 0, 0, 0.14); 31 | $shadow-3 : rgba(0, 0, 0, 0.07); 32 | $shadow-4 : rgba(0, 0, 0, 0.035); 33 | $glow-1 : rgba(255,255,255,0.1); 34 | $glow-2 : rgba(255,255,255,0.2); 35 | $glow-3 : rgba(255,255,255,0.4); 36 | $glow-4 : rgba(255,255,255,0.6); 37 | $glow-5 : rgba(255,255,255,1); 38 | 39 | // Blue 40 | // ------------------------- 41 | 42 | $blue-1: #1a739e; 43 | $blue-2: #2185c5; 44 | $blue-3: #49a8d5; 45 | $blue-4: #a7c9e0; 46 | $blue-5: #c3f4ff; 47 | 48 | // Navy 49 | // ------------------------- 50 | 51 | $navy-1: #243640; 52 | $navy-2: #3f484f; 53 | $navy-3: #525c63; 54 | 55 | // Red 56 | // ------------------------- 57 | 58 | $red-1: #b31612; 59 | $red-2: #eb3d46; 60 | $red-3: #ff434c; 61 | $red-4: #febfc1; 62 | 63 | // Green 64 | // ------------------------- 65 | $green-1: #165450; 66 | $green-2: #AFD79B; 67 | 68 | // Yellow 69 | // ------------------------- 70 | $yellow-1: #B59033; 71 | $yellow-2: #F1C148; 72 | $yellow-3: #F9F3C3; 73 | 74 | // Orange 75 | // ------------------------- 76 | $orange-1: null; 77 | $orange-2: null; 78 | 79 | 80 | // Background Colors 81 | // ------------------------- 82 | 83 | // gray backgrounds 84 | $neutral-1: $gray-1; 85 | $neutral-2: $gray-2; 86 | $neutral-3: $gray-3; 87 | $neutral-4: $gray-4; 88 | $neutral-5: $gray-5; 89 | $neutral-6: $gray-6; 90 | $neutral-7: $gray-7; 91 | $neutral-8: $gray-8; 92 | $neutral-9: $gray-9; 93 | $neutral-10: $gray-10; 94 | $neutral-11: $gray-11; 95 | 96 | // Grays (Bootstrap only, don't use directly) TODO: can we eliminate these entirely without modifying bootstrap base files? 97 | // ------------------------- 98 | 99 | $gray-base: #000 !default; 100 | $gray-darker: lighten(#000, 13.5%) !default; // #222 bootstrap only 101 | $gray-dark: lighten(#000, 20%) !default; // #333 bootstrap only 102 | $gray: lighten(#000, 33.5%) !default; // #555 bootstrap only 103 | $gray-light: lighten(#000, 60%) !default; // #999 bootstrap only 104 | $gray-lighter: lighten(#000, 93.5%) !default; // #eee bootstrap only 105 | 106 | // Dark colors 107 | // ------------------------- 108 | 109 | $dark-1: $navy-1; 110 | $dark-2: $navy-2; 111 | $dark-3: $navy-3; 112 | 113 | // Brand colors 114 | // ------------------------- 115 | 116 | $brand-1: $teal-1; 117 | $brand-2: $teal-2; 118 | $brand-3: $teal-3; 119 | $brand-4: $teal-4; 120 | $brand-5: $teal-5; 121 | 122 | $brand-primary: #1b92cb !default; 123 | $brand-success: #5cb85c !default; 124 | $brand-warning: #f0ad4e !default; 125 | $brand-danger: #CA7070 !default; 126 | $brand-info: #5bc0de !default; 127 | 128 | // Accents 129 | // ------------------------- 130 | 131 | $accent-1: $blue-1; 132 | $accent-2: $blue-2; 133 | $accent-3: $blue-3; 134 | $accent-4: $blue-4; 135 | $accent-5: $blue-5; 136 | 137 | // Errors 138 | // ------------------------- 139 | 140 | $error-1: $red-1; 141 | $error-2: $red-2; 142 | $error-3: $red-3; 143 | $error-4: $red-4; 144 | 145 | // Warnings 146 | // ------------------------- 147 | 148 | $warn-1: $yellow-1; 149 | $warn-2: $yellow-2; 150 | $warn-3: $yellow-3; 151 | 152 | // Success 153 | // ------------------------- 154 | 155 | $success-1: $green-1; 156 | $success-2: $green-2; 157 | 158 | 159 | // login page colors 160 | // -------------------------- 161 | $dark_grey: #3b3c41; 162 | $almost_white: #ededed; 163 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/app/api.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var rest = require('rest'); 5 | var defaultRequest = require('rest/interceptor/defaultRequest'); 6 | var mime = require('rest/interceptor/mime'); 7 | var uriTemplateInterceptor = require('./api/uriTemplateInterceptor'); 8 | var errorCode = require('rest/interceptor/errorCode'); 9 | var baseRegistry = require('rest/mime/registry'); 10 | 11 | var registry = baseRegistry.child(); 12 | 13 | registry.register('text/uri-list', require('./api/uriListConverter')); 14 | registry.register('application/hal+json', require('rest/mime/type/application/hal')); 15 | 16 | return rest 17 | .wrap(mime, { registry: registry }) 18 | .wrap(uriTemplateInterceptor) 19 | .wrap(errorCode) 20 | .wrap(defaultRequest, { headers: { 'Accept': 'application/hal+json' }}); 21 | 22 | }); -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/app/api/uriListConverter.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 'use strict'; 3 | 4 | /* Convert a single or array of resources into "URI1\nURI2\nURI3..." */ 5 | return { 6 | read: function(str /*, opts */) { 7 | return str.split('\n'); 8 | }, 9 | write: function(obj /*, opts */) { 10 | // If this is an Array, extract the self URI and then join using a newline 11 | if (obj instanceof Array) { 12 | return obj.map(function(resource) { 13 | return resource._links.self.href; 14 | }).join('\n'); 15 | } else { // otherwise, just return the self URI 16 | return obj._links.self.href; 17 | } 18 | } 19 | }; 20 | 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/app/api/uriTemplateInterceptor.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var interceptor = require('rest/interceptor'); 5 | 6 | return interceptor({ 7 | request: function (request /*, config, meta */) { 8 | /* If the URI is a URI Template per RFC 6570 (http://tools.ietf.org/html/rfc6570), trim out the template part */ 9 | if (request.path.indexOf('{') === -1) { 10 | return request; 11 | } else { 12 | request.path = request.path.split('{')[0]; 13 | return request; 14 | } 15 | } 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/app/datagrid.jsx: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var React = require('react'); 5 | var client = require('./api'); 6 | var stompClient = require('./websocket-listener.js'); 7 | var _ = require('lodash'); 8 | 9 | var ItemTable = React.createClass({ 10 | loadItemsFromServer: function() { 11 | var self = this; 12 | client({method: 'GET', path: this.props.url}).done(function(response) { 13 | self.setState({data: response.entity._embedded.items}); 14 | }) 15 | }, 16 | getInitialState: function() { 17 | return {data: []} 18 | }, 19 | componentDidMount: function() { 20 | this.loadItemsFromServer(); 21 | stompClient.register([ 22 | { route: '/topic/backend.newItem', callback: this.showNewItem }, 23 | { route: '/topic/backend.deleteItem', callback: this.deleteItem } 24 | ]); 25 | }, 26 | showNewItem: function(message) { 27 | var href = message.body; 28 | var self = this; 29 | client({method: 'GET', path: href}).done(function(response) { 30 | var items = self.state.data; 31 | self.setState({data: items.concat([response.entity])}); 32 | }) 33 | }, 34 | deleteItem: function(message) { 35 | var href = message.body; 36 | var items = this.state.data; 37 | _.remove(items, function(item) { 38 | return item._links.self.href.split('{')[0].endsWith(href); 39 | }); 40 | this.setState({data: items}); 41 | }, 42 | onDelete: function(href) { 43 | client({method: 'DELETE', path: href.split('{')[0]}).done(function(response) { 44 | console.log("Deleting complete!"); 45 | }) 46 | }, 47 | render: function() { 48 | var self = this; 49 | var items = this.state.data.map(function(item) { 50 | return ( 51 | 53 | ) 54 | }) 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | {items} 62 | 63 |
ItemOwnerLinksOps
64 | ) 65 | } 66 | }); 67 | 68 | var Item = React.createClass({ 69 | render: function() { 70 | var self = this; 71 | var links = Object.keys(this.props._links).map(function(rel) { 72 | var uri = self.props._links[rel].href.split('{')[0]; 73 | return ( 74 |
  • {rel}
  • 75 | ) 76 | }) 77 | return ( 78 | 79 | 80 | {this.props.user.name} 81 | {links} 82 | 83 | 84 | ) 85 | } 86 | }) 87 | 88 | var DeleteButton = React.createClass({ 89 | handleClick: function() { 90 | this.props.onDelete(this.props._links.self.href); 91 | }, 92 | render: function() { 93 | return ( 94 | 95 | ) 96 | } 97 | }) 98 | 99 | React.render( 100 | , 101 | document.getElementById('grid') 102 | ); 103 | 104 | }); -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/app/hal.json: -------------------------------------------------------------------------------- 1 | { 2 | "_links" : { 3 | "self" : { 4 | "href" : "http://spring-a-gram.cfapps.io/api/items{?page,size,sort,projection}", 5 | "templated" : true 6 | }, 7 | "search" : { 8 | "href" : "http://spring-a-gram.cfapps.io/api/items/search" 9 | } 10 | }, 11 | "_embedded" : { 12 | "items" : [ { 13 | "image": "http://spring-a-gram.cfapps.io/raw/1", 14 | "htmlUrl" : { 15 | "href" : "http://spring-a-gram.cfapps.io/image/1" 16 | }, 17 | "_links" : { 18 | "self" : { 19 | "href" : "http://spring-a-gram.cfapps.io/api/items/1{?projection}", 20 | "templated" : true 21 | }, 22 | "gallery" : { 23 | "href" : "http://spring-a-gram.cfapps.io/api/items/1/gallery" 24 | } 25 | } 26 | }, { 27 | "image": "http://spring-a-gram.cfapps.io/raw/2", 28 | "htmlUrl" : { 29 | "href" : "http://spring-a-gram.cfapps.io/image/2" 30 | }, 31 | "_links" : { 32 | "self" : { 33 | "href" : "http://spring-a-gram.cfapps.io/api/items/2{?projection}", 34 | "templated" : true 35 | }, 36 | "gallery" : { 37 | "href" : "http://spring-a-gram.cfapps.io/api/items/2/gallery" 38 | } 39 | } 40 | } ] 41 | }, 42 | "page" : { 43 | "size" : 20, 44 | "totalElements" : 2, 45 | "totalPages" : 1, 46 | "number" : 0 47 | } 48 | } -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/app/main.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | document.addEventListener('DOMContentLoaded', new function() { 5 | 6 | require('jsx!app/datagrid'); 7 | require('jsx!app/upload'); 8 | 9 | }, false); 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/app/upload.jsx: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var React = require('react'); 5 | var client = require('./api'); 6 | 7 | var FileForm = React.createClass({ 8 | getInitialState: function() { 9 | return {data_uri: null}; 10 | }, 11 | handleSubmit: function(e) { 12 | e.preventDefault(); 13 | var self = this; 14 | client({ 15 | method: 'POST', 16 | path: self.props.url, 17 | entity: {image: self.state.data_uri}, 18 | headers: {'Content-Type': 'application/json'} 19 | }).done(function(response) { 20 | self.setState({data_uri: null}); 21 | }); 22 | }, 23 | handleFile: function(e) { 24 | var self = this; 25 | var reader = new FileReader(); 26 | var file = e.target.files[0]; 27 | 28 | reader.onload = function(upload) { 29 | self.setState({data_uri: upload.target.result}); 30 | } 31 | 32 | reader.readAsDataURL(file); 33 | }, 34 | render: function() { 35 | return ( 36 |
    37 | 38 | 39 |
    40 | ) 41 | } 42 | }); 43 | 44 | React.render( 45 | , 46 | document.getElementById('upload') 47 | ); 48 | 49 | }); -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/app/websocket-listener.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var sockjs = require('sockjs-client'); 5 | var stomp = require('stomp-websocket'); 6 | 7 | return { 8 | register: register 9 | }; 10 | 11 | function register(registrations) { 12 | console.log(sockjs); 13 | console.log(stomp); 14 | var socket = new SockJS('/spring-a-gram'); 15 | var stompClient = Stomp.over(socket); 16 | stompClient.connect({}, function(frame) { 17 | console.log('Connected: ' + frame); 18 | registrations.forEach(function (registration) { 19 | stompClient.subscribe(registration.route, registration.callback); 20 | }); 21 | }); 22 | } 23 | 24 | }); -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-a-gram-backend", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/gregturn/spring-a-gram", 5 | "authors": [ 6 | "Greg Turnquist " 7 | ], 8 | "description": "Upload your fav pics", 9 | "main": "app/main.js", 10 | "moduleType": [ 11 | "amd" 12 | ], 13 | "keywords": [ 14 | "rest", 15 | "hateoas", 16 | "spring", 17 | "data" 18 | ], 19 | "license": "ASL", 20 | "private": true, 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests" 27 | ], 28 | "dependencies": { 29 | "inuit-starter-kit": "~0.2.8", 30 | "inuit-layout": "~0.3.2", 31 | "inuit-widths-responsive": "~0.1.3", 32 | "inuit-buttons": "~0.4.1", 33 | "inuit-box": "~0.4.4", 34 | "inuit-tables": "~0.2.1", 35 | "requirejs": "~2.1.19", 36 | "rest": "~1.3.1", 37 | "stomp-websocket": "~2.3.4", 38 | "sockjs-client": "~0.3.4", 39 | "lodash": "~3.10.0", 40 | "jsx-requirejs-plugin": "~0.6.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/main.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // Pre-sets must be done BEFORE a module is imported. Some of these settings are at the top, while others 3 | // are right before the import statement. 4 | //////////////////////////////////////////////////////////////////////// 5 | 6 | @import "pivotal.palette"; 7 | 8 | // Spring's palette (found on spring.io) 9 | $spring-green: #6db33f; 10 | $spring-black: #34302d; 11 | $spring-antique-white: #F1F1F1; 12 | $spring-white: #eee; 13 | 14 | // spring.io coloring 15 | $brand-color: $spring-green; 16 | $brand-highlight: $spring-white; 17 | $brand-highlight2: $spring-antique-white; 18 | $brand-lowlight: $spring-black; 19 | 20 | // pivotal.io coloring 21 | $brand-color: $green-2; 22 | $brand-highlight: white; 23 | $brand-highlight2: $spring-antique-white; 24 | $brand-lowlight: $teal-3; 25 | 26 | //$inuit-base-font-size: 12px; 27 | $inuit-base-line-height: 18px; 28 | $inuit-base-background-color: $brand-highlight2; 29 | 30 | //////////////////////////////////////////////////////////////////////// 31 | // Here are the layers of inuit. Each layer needs to have its order preserved, but modules within the same layer 32 | // can be imported in any order. 33 | //////////////////////////////////////////////////////////////////////// 34 | 35 | // Settings layer 36 | @import "bower_components/inuit-defaults/settings.defaults"; 37 | @import "bower_components/inuit-responsive-settings/settings.responsive"; 38 | 39 | // Tools layer 40 | @import "bower_components/inuit-functions/tools.functions"; 41 | @import "bower_components/inuit-mixins/tools.mixins"; 42 | @import "bower_components/inuit-responsive-tools/tools.responsive"; 43 | 44 | // Generic layer 45 | @import "bower_components/inuit-normalize/generic.normalize"; 46 | @import "bower_components/inuit-box-sizing/generic.box-sizing"; 47 | 48 | @import "bower_components/inuit-page/base.page"; 49 | 50 | // Objects layer 51 | $inuit-enable-layout--large: true; 52 | @import "bower_components/inuit-layout/objects.layout"; 53 | 54 | $inuit-enable-table--rows: true; 55 | $inuit-enable-table--cosy: true; 56 | @import "bower_components/inuit-tables/objects.tables"; 57 | 58 | //$inuit-enable-btn--full: true; 59 | $inuit-enable-btn--small: true; 60 | $inuit-btn-background: $brand-color; 61 | $inuit-btn-color: $brand-highlight; 62 | @import "bower_components/inuit-buttons/objects.buttons"; 63 | 64 | $inuit-enable-box--tiny: true; 65 | @import "bower_components/inuit-box/objects.box"; 66 | 67 | // Trumps layer 68 | @import "bower_components/inuit-widths/trumps.widths"; 69 | @import "bower_components/inuit-clearfix/trumps.clearfix"; 70 | @import "bower_components/inuit-widths-responsive/trumps.widths-responsive"; 71 | 72 | //////////////////////////////////////////////////////////////////////// 73 | // Custom CSS additions and overrides 74 | // 75 | // This isn't to override settings from inuit, but instead, to override or add to CSS generated 76 | // by inuit. 77 | //////////////////////////////////////////////////////////////////////// 78 | 79 | // Plugin the Ubuntu font 80 | html { 81 | font-family: 'Ubuntu', sans-serif; 82 | } 83 | 84 | // Dress up the top row of a "page" 85 | // Add a small amount of padding at the bottom of a "page" 86 | $inuit-base-spacing-unit--tiny: round($inuit-base-spacing-unit / 8); 87 | .page { 88 | border-top: $brand-color solid 4px; 89 | color: $brand-highlight; 90 | background-color: $brand-lowlight; 91 | width: 100%; 92 | margin-top: 0em; 93 | padding-bottom: $inuit-base-spacing-unit--tiny; 94 | } 95 | 96 | // Wrap a given block with insets so it isn't pushed to the edge of the browser 97 | .wrapper { 98 | max-width: 1100px; 99 | margin: 0 auto; 100 | padding-left: 12px; 101 | padding-right: 12px; 102 | } 103 | 104 | // Fine tune the bar at the top, so it shrinks vertically on portable devices 105 | h1 { 106 | @include media-query(portable) { 107 | margin: 0.1em 0; 108 | } 109 | } 110 | 111 | // Custom adjustments for single items 112 | img { 113 | height: $inuit-base-line-height*2; 114 | } 115 | 116 | th { 117 | text-align: left; 118 | } 119 | 120 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spring-a-Gram", 3 | "version": "1.0.0", 4 | "description": "Demo Spring Data REST by uploading your favorite pics", 5 | "main": "run.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:gregturn/spring-a-gram.git" 9 | }, 10 | "keywords": [ 11 | "spring", 12 | "data", 13 | "rest", 14 | "hateoas", 15 | "hypermedia" 16 | ], 17 | "author": "Greg L. Turnquist", 18 | "license": "ASLv2", 19 | "bugs": { 20 | "url": "https://github.com/gregturn/spring-a-gram/issues" 21 | }, 22 | "homepage": "https://github.com/gregturn/spring-a-gram", 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "bower": "^1.3.12" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/static/run.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | require.config({ 4 | deps: [ 'app/main' ], 5 | 6 | packages: [ 7 | { name: 'rest', location: 'bower_components/rest', main: 'browser' }, 8 | { name: 'when', location: 'bower_components/when', main: 'when' }, 9 | { name: 'sockjs-client', location: 'bower_components/sockjs-client', main: 'dist/sockjs.js'}, 10 | { name: 'stomp-websocket', location: 'bower_components/stomp-websocket', main: 'lib/stomp.min.js' }, 11 | { name: 'react', location: 'bower_components/react', main: 'react' }, 12 | { name: 'JSXTransformer', location: 'bower_components/jsx-requirejs-plugin/js', main: 'JSXTransformer' }, 13 | { name: 'jsx', location: 'bower_components/jsx-requirejs-plugin/js', main: 'jsx' }, 14 | { name: 'text', location: 'bower_components/requirejs-text', main: 'text' }, 15 | { name: 'lodash', location: 'bower_components/lodash', main: 'lodash' } 16 | ], 17 | 18 | jsx: { 19 | fileExtension: ".jsx", 20 | harmony: true, 21 | stripTypes: true 22 | } 23 | }); 24 | 25 | }()); -------------------------------------------------------------------------------- /spring-a-gram-backend/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Spring-a-Gram :: Data Admin UI 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 |

    Spring-a-Gram Data Admin UI

    16 |
    17 | 18 |
    19 |

    This small UI provides a close-to-the-metal way to manage Spring-a-Gram data.

    20 |
    21 | 22 |
    23 |

    Upload

    24 |
    25 |
    26 | 27 |
    28 |

    Existing

    29 |
    30 |
    31 | 32 | 33 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/test/java/com/greglturnquist/springagram/backend/GalleryDocumentation.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import static org.hamcrest.CoreMatchers.*; 4 | import static org.hamcrest.MatcherAssert.*; 5 | import static org.springframework.restdocs.RestDocumentation.*; 6 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; 8 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 9 | 10 | import java.net.URI; 11 | import java.util.Collection; 12 | 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 18 | import org.springframework.boot.test.SpringApplicationConfiguration; 19 | import org.springframework.context.annotation.Configuration; 20 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 21 | import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; 22 | import org.springframework.hateoas.Link; 23 | import org.springframework.hateoas.PagedResources; 24 | import org.springframework.hateoas.hal.Jackson2HalModule; 25 | import org.springframework.http.MediaType; 26 | import org.springframework.restdocs.config.RestDocumentationConfigurer; 27 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 28 | import org.springframework.test.context.web.WebAppConfiguration; 29 | import org.springframework.test.web.servlet.MockMvc; 30 | import org.springframework.test.web.servlet.MvcResult; 31 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 32 | import org.springframework.web.context.WebApplicationContext; 33 | 34 | import com.fasterxml.jackson.core.type.TypeReference; 35 | import com.fasterxml.jackson.databind.DeserializationFeature; 36 | import com.fasterxml.jackson.databind.ObjectMapper; 37 | 38 | @RunWith(SpringJUnit4ClassRunner.class) 39 | @WebAppConfiguration 40 | @SpringApplicationConfiguration(classes = GalleryDocumentation.TestConfiguration.class) 41 | public class GalleryDocumentation { 42 | 43 | protected MockMvc mvc; 44 | protected static MediaType DEFAULT_MEDIA_TYPE = org.springframework.hateoas.MediaTypes.HAL_JSON; 45 | 46 | @Autowired WebApplicationContext context; 47 | @Autowired GalleryRepository galleryRepository; 48 | @Autowired RepositoryEntityLinks entityLinks; 49 | 50 | @Before 51 | public void setUp() { 52 | 53 | mvc = MockMvcBuilders 54 | .webAppContextSetup(context) 55 | .apply(new RestDocumentationConfigurer()) 56 | .defaultRequest(get("/").accept(DEFAULT_MEDIA_TYPE)) 57 | .build(); 58 | } 59 | 60 | @Test 61 | public void getACollectionOfGalleries() throws Exception { 62 | 63 | Gallery newGallery = new Gallery(); 64 | newGallery.setDescription("Collection of cats"); 65 | Gallery savedGallery = galleryRepository.save(newGallery); 66 | 67 | Link galleriesLink = entityLinks.linkToCollectionResource(Gallery.class); 68 | 69 | MvcResult result = mvc.perform(get(galleriesLink.expand().getHref())) 70 | .andDo(print()) 71 | .andDo(document("getCollectionOfGalleries")) 72 | .andExpect(status().isOk()) 73 | .andReturn(); 74 | 75 | ObjectMapper mapper = new ObjectMapper(); 76 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 77 | mapper.registerModules(new Jackson2HalModule()); 78 | PagedResources resourceGalleries = mapper.readValue(result.getResponse().getContentAsString(), 79 | new TypeReference>() {}); 80 | 81 | assertThat(resourceGalleries.getLinks().size(), equalTo(1)); 82 | 83 | assertThat(resourceGalleries.hasLink("self"), is(true)); 84 | assertThat(resourceGalleries.getLink("self").isTemplated(), is(false)); 85 | final String self = resourceGalleries.getLink("self").expand().getHref(); 86 | assertThat(self, containsString(new URI(self).getPath())); 87 | 88 | Collection galleries = resourceGalleries.getContent(); 89 | assertThat(galleries.size(), equalTo(1)); 90 | Gallery gallery = galleries.toArray(new Gallery[]{})[0]; 91 | assertThat(gallery.getItems(), is(nullValue())); 92 | assertThat(gallery.getDescription(), equalTo(savedGallery.getDescription())); 93 | } 94 | 95 | @Configuration 96 | @EnableJpaRepositories(basePackageClasses = Item.class) 97 | @EnableAutoConfiguration 98 | public static class TestConfiguration { 99 | 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/test/java/com/greglturnquist/springagram/backend/ItemDocumentation.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.backend; 2 | 3 | import static java.util.stream.Collectors.*; 4 | import static org.hamcrest.CoreMatchers.*; 5 | import static org.hamcrest.MatcherAssert.*; 6 | import static org.springframework.restdocs.RestDocumentation.*; 7 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 8 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 10 | 11 | import java.net.URI; 12 | import java.util.ArrayList; 13 | import java.util.Collection; 14 | import java.util.List; 15 | 16 | import org.junit.Before; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.beans.factory.annotation.Value; 22 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 23 | import org.springframework.boot.test.SpringApplicationConfiguration; 24 | import org.springframework.context.annotation.Configuration; 25 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 26 | import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; 27 | import org.springframework.hateoas.Link; 28 | import org.springframework.hateoas.LinkDiscoverer; 29 | import org.springframework.hateoas.LinkDiscoverers; 30 | import org.springframework.hateoas.MediaTypes; 31 | import org.springframework.hateoas.PagedResources; 32 | import org.springframework.hateoas.Resource; 33 | import org.springframework.hateoas.Resources; 34 | import org.springframework.hateoas.hal.Jackson2HalModule; 35 | import org.springframework.http.HttpHeaders; 36 | import org.springframework.http.MediaType; 37 | import org.springframework.restdocs.config.RestDocumentationConfigurer; 38 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 39 | import org.springframework.security.core.context.SecurityContextHolder; 40 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 41 | import org.springframework.test.context.web.WebAppConfiguration; 42 | import org.springframework.test.web.servlet.MockMvc; 43 | import org.springframework.test.web.servlet.MvcResult; 44 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 45 | import org.springframework.web.context.WebApplicationContext; 46 | 47 | import com.fasterxml.jackson.core.type.TypeReference; 48 | import com.fasterxml.jackson.databind.DeserializationFeature; 49 | import com.fasterxml.jackson.databind.ObjectMapper; 50 | 51 | @RunWith(SpringJUnit4ClassRunner.class) 52 | @WebAppConfiguration 53 | @SpringApplicationConfiguration(classes = ItemDocumentation.TestConfiguration.class) 54 | public class ItemDocumentation { 55 | 56 | protected MockMvc mvc; 57 | protected static MediaType DEFAULT_MEDIA_TYPE = org.springframework.hateoas.MediaTypes.HAL_JSON; 58 | 59 | @Autowired WebApplicationContext context; 60 | @Autowired ItemRepository itemRepository; 61 | @Autowired UserRepository userRepository; 62 | @Autowired RepositoryEntityLinks entityLinks; 63 | @Autowired LinkDiscoverers discoverers; 64 | @Value("${spring.data.rest.basePath}") String basePath; 65 | 66 | @Before 67 | public void setUp() { 68 | 69 | mvc = MockMvcBuilders 70 | .webAppContextSetup(context) 71 | .apply(new RestDocumentationConfigurer()) 72 | .defaultRequest(get("/").accept(DEFAULT_MEDIA_TYPE)) 73 | .build(); 74 | itemRepository.deleteAll(); 75 | } 76 | 77 | @Test 78 | public void getACollectionOfItemsWithAProjection() throws Exception { 79 | 80 | Item newItem = new Item(); 81 | newItem.setImage("test image"); 82 | Item savedItem = itemRepository.save(newItem); 83 | 84 | Link itemsLink = entityLinks.linkToCollectionResource(Item.class); 85 | 86 | MvcResult result = mvc.perform(get(itemsLink.expand().getHref() + "?projection=noImages")) 87 | .andDo(document("getCollectionOfItemsWithNoImages")) 88 | .andDo(print()) 89 | .andExpect(status().isOk()) 90 | .andReturn(); 91 | 92 | ObjectMapper mapper = new ObjectMapper(); 93 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 94 | mapper.registerModules(new Jackson2HalModule()); 95 | PagedResources resourceItems = mapper.readValue(result.getResponse().getContentAsString(), 96 | new TypeReference>() {}); 97 | 98 | assertThat(resourceItems.getLinks().size(), equalTo(2)); 99 | 100 | assertThat(resourceItems.hasLink("self"), is(true)); 101 | assertThat(resourceItems.getLink("self").isTemplated(), is(false)); 102 | final String self = resourceItems.getLink("self").expand().getHref(); 103 | assertThat(self, containsString(new URI(self).getPath())); 104 | 105 | assertThat(resourceItems.hasLink("search"), is(true)); 106 | assertThat(resourceItems.getLink("search").isTemplated(), is(false)); 107 | final String search = resourceItems.getLink("search").expand().getHref(); 108 | assertThat(search, containsString(new URI(search).getPath())); 109 | 110 | Collection items = resourceItems.getContent(); 111 | assertThat(items.size(), equalTo(1)); 112 | Item item = items.toArray(new Item[]{})[0]; 113 | assertThat(item.getImage(), is(nullValue())); 114 | assertThat(item.getGallery(), equalTo(savedItem.getGallery())); 115 | assertThat(item.getUser(), equalTo(savedItem.getUser())); 116 | } 117 | 118 | @Test 119 | public void hoppingFromRootToSingleItem() throws Exception { 120 | 121 | Item newItem = new Item(); 122 | newItem.setImage("test image"); 123 | Item savedItem = itemRepository.save(newItem); 124 | 125 | LinkDiscoverer linkDiscoverer = discoverers.getLinkDiscovererFor(MediaTypes.HAL_JSON); 126 | 127 | MvcResult rootResponse = mvc.perform(get(basePath)) 128 | .andDo(document("hoppingFromRootToSingleItem.root")) 129 | .andExpect(status().isOk()) 130 | .andReturn(); 131 | 132 | Link itemsLink = linkDiscoverer.findLinkWithRel("items", rootResponse.getResponse().getContentAsString()); 133 | 134 | MvcResult itemsResponse = mvc.perform(get(itemsLink.expand().getHref() + "?projection=noImages")) 135 | .andDo(document("hoppingFromRootToSingleItem.items")) 136 | .andExpect(status().isOk()) 137 | .andReturn(); 138 | 139 | Link searchLink = linkDiscoverer.findLinkWithRel("search", itemsResponse.getResponse().getContentAsString()); 140 | 141 | MvcResult searchResponse = mvc.perform(get(searchLink.expand().getHref())) 142 | .andDo(document("hoppingFromRootToSingleItem.search")) 143 | .andExpect(status().isOk()) 144 | .andReturn(); 145 | 146 | Link findByGalleryIsNullLink = linkDiscoverer.findLinkWithRel("findByGalleryIsNull", 147 | searchResponse.getResponse().getContentAsString()); 148 | 149 | MvcResult findByGalleryIsNullResponse = mvc.perform(get(findByGalleryIsNullLink.expand().getHref() + "?projection=noImages")) 150 | .andDo(document("hoppingFromRootToSingleItem.findByGalleryIsNull")) 151 | .andDo(print()) 152 | .andExpect(status().isOk()) 153 | .andReturn(); 154 | 155 | ObjectMapper mapper = new ObjectMapper(); 156 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 157 | mapper.registerModules(new Jackson2HalModule()); 158 | Resources> items = mapper.readValue(findByGalleryIsNullResponse.getResponse().getContentAsString(), 159 | new TypeReference>>() { 160 | }); 161 | 162 | assertThat(items.getLinks().size(), equalTo(1)); 163 | assertThat(items.getContent().size(), equalTo(1)); 164 | 165 | List links = items.getContent().stream() 166 | .map(item -> item.getLink("self")) 167 | .collect(toList()); 168 | 169 | List unlinkedItems = new ArrayList<>(); 170 | 171 | for (Link link : links) { 172 | MvcResult result = mvc.perform(get(link.expand().getHref())) 173 | .andDo(document("hoppingFromRootToSingleItem" + new URI(link.expand().getHref()).getPath())) 174 | .andExpect(status().isOk()) 175 | .andReturn(); 176 | 177 | Resource itemResource = mapper.readValue(result.getResponse().getContentAsString(), 178 | new TypeReference>() {}); 179 | unlinkedItems.add(itemResource.getContent()); 180 | } 181 | 182 | assertThat(unlinkedItems.size(), equalTo(1)); 183 | assertThat(unlinkedItems.get(0).getImage(), equalTo("test image")); 184 | } 185 | 186 | @Test 187 | public void createAndDestroy() throws Exception { 188 | 189 | String item = "{\"image\": \"test image\"}"; 190 | 191 | // Security bits must be in place to support linking hte newly created item with the user 192 | User user = new User(); 193 | user.setName("jack reacher"); 194 | user.setPassword("1031"); 195 | user.setRoles(new String[]{"ROLE_USER"}); 196 | user = userRepository.save(user); 197 | 198 | SecurityContextHolder.getContext().setAuthentication( 199 | new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword())); 200 | 201 | Link itemsLink = entityLinks.linkToCollectionResource(Item.class); 202 | 203 | MvcResult createResults = mvc.perform( 204 | post(itemsLink.expand().getHref()) 205 | .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 206 | .content(item)) 207 | .andDo(document("createAndDestroy.create")) 208 | .andExpect(status().isCreated()) 209 | .andReturn(); 210 | 211 | final String locationLink = createResults.getResponse().getHeader(HttpHeaders.LOCATION); 212 | assertThat(locationLink, is(notNullValue())); 213 | 214 | ObjectMapper mapper = new ObjectMapper(); 215 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 216 | mapper.registerModules(new Jackson2HalModule()); 217 | 218 | Resource itemResource = mapper.readValue(createResults.getResponse().getContentAsString(), 219 | new TypeReference>() { }); 220 | 221 | assertThat(itemResource.hasLink("self"), is(true)); 222 | assertThat(itemResource.getLink("self").isTemplated(), is(true)); 223 | assertThat(itemResource.getLink("self").expand().getHref(), equalTo(locationLink)); 224 | 225 | mvc.perform(delete(locationLink)) 226 | .andDo(document("createAndDestroy.delete")) 227 | .andExpect(status().isNoContent()) 228 | .andReturn(); 229 | 230 | mvc.perform(get(locationLink)) 231 | .andDo(document("createAndDestroy.recheck")) 232 | .andExpect(status().isNotFound()) 233 | .andReturn(); 234 | } 235 | 236 | @Configuration 237 | @EnableJpaRepositories(basePackageClasses = Item.class) 238 | @EnableAutoConfiguration 239 | public static class TestConfiguration { 240 | 241 | } 242 | 243 | } 244 | -------------------------------------------------------------------------------- /spring-a-gram-backend/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /spring-a-gram-eureka-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.greglturnquist.springagram 8 | spring-a-gram 9 | 0.1.0 10 | 11 | 12 | com.greglturnquist.springagram 13 | spring-a-gram-eureka-server 14 | 0.1.0 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-web 20 | 21 | 22 | org.springframework.cloud 23 | spring-cloud-starter-eureka-server 24 | 25 | 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-maven-plugin 32 | 33 | 34 | 35 | com.netflix.eureka 36 | eureka-core 37 | 38 | 39 | com.netflix.eureka 40 | eureka-client 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /spring-a-gram-eureka-server/src/main/java/com/greglturnquist/springagram/EurekaServiceDiscoveryApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram; 17 | 18 | import org.springframework.boot.autoconfigure.SpringBootApplication; 19 | import org.springframework.boot.builder.SpringApplicationBuilder; 20 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 21 | 22 | @SpringBootApplication 23 | @EnableEurekaServer 24 | public class EurekaServiceDiscoveryApplication { 25 | 26 | public static void main(String[] args) { 27 | new SpringApplicationBuilder(EurekaServiceDiscoveryApplication.class).web(true).run(args); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /spring-a-gram-eureka-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8761 3 | 4 | eureka: 5 | instance: 6 | hostname: localhost 7 | client: 8 | registerWithEureka: false 9 | fetchRegistry: false 10 | serviceUrl: 11 | defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ 12 | 13 | ribbon: 14 | ServerListRefreshInterval: 5000 15 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.greglturnquist.springagram 9 | spring-a-gram 10 | 0.1.0 11 | 12 | 13 | com.greglturnquist.springagram 14 | spring-a-gram-frontend 15 | 0.1.0 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-data-jpa 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-hateoas 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-thymeleaf 29 | 30 | 31 | org.thymeleaf.extras 32 | thymeleaf-extras-springsecurity3 33 | 34 | 35 | io.pivotal.spring.cloud 36 | spring-cloud-services-starter-service-registry 37 | 38 | 39 | io.pivotal.spring.cloud 40 | spring-cloud-services-starter-circuit-breaker 41 | 42 | 43 | io.pivotal.spring.cloud 44 | spring-cloud-services-starter-config-client 45 | 46 | 47 | org.springframework.cloud 48 | spring-cloud-starter-zuul 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-security 53 | 54 | 55 | org.springframework.security.oauth 56 | spring-security-oauth2 57 | ${spring-security-oauth2.version} 58 | 59 | 60 | 61 | 62 | 63 | 64 | com.github.eirslett 65 | frontend-maven-plugin 66 | 0.0.23 67 | 68 | src/main/resources/static 69 | 70 | 71 | 72 | install node and npm 73 | 74 | install-node-and-npm 75 | 76 | 77 | v0.10.33 78 | 1.3.8 79 | 80 | 81 | 82 | npm install 83 | 84 | npm 85 | 86 | 87 | install 88 | 89 | 90 | 91 | bower install 92 | 93 | bower 94 | 95 | generate-resources 96 | 97 | install 98 | 99 | 100 | 101 | 102 | 103 | nl.geodienstencentrum.maven 104 | sass-maven-plugin 105 | 2.5 106 | 107 | 108 | package 109 | process-resources 110 | 111 | update-stylesheets 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | ${basedir}/src/main/resources/static 120 | 121 | ${basedir}/src/main/resources/static 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/ApplicationController.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import java.net.URI; 4 | import java.util.Arrays; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.hateoas.Link; 12 | import org.springframework.hateoas.MediaTypes; 13 | import org.springframework.hateoas.Resource; 14 | import org.springframework.hateoas.client.Traverson; 15 | import org.springframework.http.HttpEntity; 16 | import org.springframework.http.HttpHeaders; 17 | import org.springframework.http.HttpMethod; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.stereotype.Controller; 20 | import org.springframework.web.bind.annotation.ModelAttribute; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | import org.springframework.web.bind.annotation.RequestMethod; 23 | import org.springframework.web.bind.annotation.RequestParam; 24 | import org.springframework.web.client.RestTemplate; 25 | import org.springframework.web.servlet.ModelAndView; 26 | 27 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; 28 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; 29 | 30 | /** 31 | * This is the web controller that contains web pages and other custom end points. 32 | */ 33 | @Controller 34 | public class ApplicationController { 35 | 36 | private static final Logger log = LoggerFactory.getLogger(ApplicationController.class); 37 | 38 | private final RestTemplate rest = new RestTemplate(); 39 | 40 | @Autowired 41 | ApplicationControllerHelper helper; 42 | 43 | @Value("${springagram.hashtag:#springagram}") 44 | String hashtag; 45 | 46 | @Value("${spring.data.rest.basePath}") 47 | String basePath; 48 | 49 | /** 50 | * Serve up the home page 51 | * @return 52 | */ 53 | @RequestMapping(method=RequestMethod.GET, value="/") 54 | public ModelAndView index() { 55 | 56 | ModelAndView modelAndView = new ModelAndView("index"); 57 | modelAndView.addObject("gallery", new Gallery()); 58 | modelAndView.addObject("newGallery", 59 | linkTo(methodOn(ApplicationController.class).newGallery(null, null)) 60 | .withRel("New Gallery")); 61 | return modelAndView; 62 | } 63 | 64 | @RequestMapping(method=RequestMethod.POST, value="/") 65 | public ModelAndView newGallery(@ModelAttribute Gallery gallery, HttpEntity httpEntity) { 66 | 67 | Link root = linkTo(methodOn(ApplicationController.class).index()).slash("/api").withRel("root"); 68 | Link galleries = new Traverson(URI.create(root.expand().getHref()), MediaTypes.HAL_JSON).// 69 | follow("galleries").// 70 | withHeaders(httpEntity.getHeaders()).// 71 | asLink(); 72 | 73 | HttpHeaders headers = new HttpHeaders(); 74 | headers.putAll(httpEntity.getHeaders()); 75 | headers.setContentType(MediaType.APPLICATION_JSON); 76 | HttpEntity subRequest = new HttpEntity<>(gallery, headers); 77 | rest.exchange(galleries.expand().getHref(), HttpMethod.POST, subRequest, Gallery.class); 78 | 79 | return index(); 80 | } 81 | 82 | @RequestMapping(method=RequestMethod.GET, value="/image") 83 | public ModelAndView imageViaLink(@RequestParam("link") String link, HttpEntity httpEntity) { 84 | 85 | Resource itemResource = helper.getImageResourceViaLink(link, httpEntity); 86 | 87 | return new ModelAndView("oneImage") 88 | .addObject("item", itemResource.getContent()) 89 | .addObject("hashtag", hashtag) 90 | .addObject("links", Arrays.asList( 91 | linkTo(methodOn(ApplicationController.class).index()).withRel("All Images"), 92 | new Link(itemResource.getContent().getImage()).withRel("Raw Image"), 93 | new Link(link).withRel("HAL record") 94 | )); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/ApplicationControllerHelper.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; 4 | 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.core.ParameterizedTypeReference; 7 | import org.springframework.hateoas.Resource; 8 | import org.springframework.hateoas.mvc.TypeReferences; 9 | import org.springframework.http.HttpEntity; 10 | import org.springframework.http.HttpMethod; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.web.client.RestTemplate; 14 | 15 | @Component 16 | public class ApplicationControllerHelper { 17 | 18 | private static final String QUERY_PROJECTION_OWNER = "?projection=owner"; 19 | 20 | private final RestTemplate rest = new RestTemplate(); 21 | 22 | @Value("${springagram.fallbackImageUrl:https://d1fto35gcfffzn.cloudfront.net/images/oss/oss-logo-spring.png}") 23 | private String fallbackImageUrl; 24 | 25 | @HystrixCommand(fallbackMethod = "getFallbackImageResource") 26 | public Resource getImageResourceViaLink(String link, HttpEntity httpEntity) { 27 | String url = link + QUERY_PROJECTION_OWNER; 28 | HttpEntity requestEntity = new HttpEntity<>(httpEntity.getHeaders()); 29 | ParameterizedTypeReference> typeReference = new TypeReferences.ResourceType() {}; 30 | ResponseEntity> resource = rest.exchange(url, HttpMethod.GET, requestEntity, typeReference); 31 | return resource.getBody(); 32 | } 33 | 34 | public Resource getFallbackImageResource(String link, HttpEntity httpEntity) { 35 | Item item = new Item(); 36 | item.setImage(fallbackImageUrl); 37 | User user = new User(); 38 | user.setName("fallback"); 39 | user.setRoles(new String[]{}); 40 | item.setUser(user); 41 | return new Resource<>(item); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/BackendTrafficListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.frontend; 17 | 18 | import java.nio.charset.Charset; 19 | 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.context.annotation.Bean; 25 | import org.springframework.data.redis.connection.MessageListener; 26 | import org.springframework.data.redis.connection.RedisConnectionFactory; 27 | import org.springframework.data.redis.listener.PatternTopic; 28 | import org.springframework.data.redis.listener.RedisMessageListenerContainer; 29 | import org.springframework.messaging.simp.SimpMessagingTemplate; 30 | import org.springframework.stereotype.Component; 31 | 32 | /** 33 | * Relay traffic picked up from the RabbitMQ broker to the WebSocket broker. 34 | * 35 | * @author Greg Turnquist 36 | */ 37 | // tag::code[] 38 | @Component 39 | public class BackendTrafficListener { 40 | 41 | private static final String BACKEND_CHANNEL = "spring-a-gram"; 42 | private static final Logger log = LoggerFactory.getLogger(BackendTrafficListener.class); 43 | 44 | private final SimpMessagingTemplate template; 45 | 46 | @Autowired 47 | public BackendTrafficListener(SimpMessagingTemplate template) { 48 | this.template = template; 49 | } 50 | 51 | @Bean 52 | RedisMessageListenerContainer container(RedisConnectionFactory factory, MessageListener messageListener) { 53 | 54 | RedisMessageListenerContainer container = new RedisMessageListenerContainer(); 55 | container.setConnectionFactory(factory); 56 | container.addMessageListener(messageListener, new PatternTopic("/topic/*")); 57 | return container; 58 | } 59 | 60 | @Bean 61 | MessageListener messageListener() { 62 | 63 | return (message, pattern) -> 64 | handle(new String(message.getBody(),Charset.defaultCharset()), 65 | new String(message.getChannel(),Charset.defaultCharset())); 66 | } 67 | 68 | public void handle(String message, String destination) { 69 | 70 | log.error("Forwarding <" + message + "> to " + destination); 71 | template.convertAndSend(destination, message); 72 | } 73 | 74 | } 75 | // end::code[] 76 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/FrontendApplication.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration; 7 | import org.springframework.boot.builder.SpringApplicationBuilder; 8 | import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; 9 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 10 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 11 | 12 | @SpringBootApplication(exclude=WebSocketAutoConfiguration.class) 13 | @EnableDiscoveryClient 14 | @EnableZuulProxy 15 | @EnableCircuitBreaker 16 | public class FrontendApplication { 17 | 18 | public static void main(String[] args) throws IOException { 19 | new SpringApplicationBuilder(FrontendApplication.class).web(true).run(args); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/Gallery.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import java.util.List; 4 | 5 | import lombok.Data; 6 | 7 | @Data 8 | public class Gallery { 9 | 10 | private String description; 11 | private List items; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/Item.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Item { 7 | 8 | private String image; 9 | private Gallery gallery; 10 | private User user; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/OAuth2ServerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Primary; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.security.authentication.AuthenticationManager; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 12 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 13 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 14 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 15 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 16 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 17 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; 18 | import org.springframework.security.oauth2.provider.token.DefaultTokenServices; 19 | import org.springframework.security.oauth2.provider.token.TokenStore; 20 | import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; 21 | 22 | @Configuration 23 | @Profile("oauth") 24 | public class OAuth2ServerConfiguration { 25 | 26 | private static final String RESOURCE_ID = "springagram"; 27 | 28 | @Configuration 29 | @EnableResourceServer 30 | @Profile("oauth") 31 | protected static class ResourceServerConfiguration extends 32 | ResourceServerConfigurerAdapter { 33 | 34 | @Override 35 | public void configure(ResourceServerSecurityConfigurer resources) { 36 | resources.resourceId(RESOURCE_ID); 37 | } 38 | 39 | @Override 40 | public void configure(HttpSecurity http) throws Exception { 41 | http 42 | .requestMatchers() 43 | .antMatchers("/api/**") 44 | .and() 45 | .authorizeRequests() 46 | .antMatchers("/api/**").access("#oauth2.hasScope('read') and hasRole('ROLE_USER')"); 47 | } 48 | 49 | } 50 | 51 | @Configuration 52 | @EnableAuthorizationServer 53 | @Profile("oauth") 54 | protected static class AuthorizationServerConfiguration extends 55 | AuthorizationServerConfigurerAdapter { 56 | 57 | private TokenStore tokenStore = new InMemoryTokenStore(); 58 | 59 | @Autowired 60 | @Qualifier("authenticationManagerBean") 61 | private AuthenticationManager authenticationManager; 62 | 63 | @Override 64 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) 65 | throws Exception { 66 | endpoints 67 | .tokenStore(this.tokenStore) 68 | .authenticationManager(this.authenticationManager); 69 | } 70 | 71 | @Override 72 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 73 | clients 74 | .inMemory() 75 | .withClient("springagramclient") 76 | .authorizedGrantTypes("password", "refresh_token") 77 | .authorities("USER") 78 | .scopes("read", "write") 79 | .resourceIds(RESOURCE_ID) 80 | .secret("123456"); 81 | } 82 | 83 | @Bean 84 | @Primary 85 | public DefaultTokenServices tokenServices() { 86 | DefaultTokenServices tokenServices = new DefaultTokenServices(); 87 | tokenServices.setSupportRefreshToken(true); 88 | tokenServices.setTokenStore(this.tokenStore); 89 | return tokenServices; 90 | } 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/RedisConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.frontend; 17 | 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.annotation.Configuration; 20 | import org.springframework.context.annotation.Profile; 21 | import org.springframework.session.data.redis.config.ConfigureRedisAction; 22 | 23 | /** 24 | * @author Greg Turnquist 25 | */ 26 | @Configuration 27 | public class RedisConfig { 28 | 29 | @Bean 30 | @Profile("cloud") 31 | public static ConfigureRedisAction configureRedisAction() { 32 | return ConfigureRedisAction.NO_OP; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/SecureTomcatConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import java.io.FileNotFoundException; 4 | 5 | import org.apache.catalina.connector.Connector; 6 | import org.apache.coyote.http11.Http11NioProtocol; 7 | import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; 8 | import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Profile; 12 | import org.springframework.util.ResourceUtils; 13 | 14 | /** 15 | * Configure two Tomcat connectors, 8080/http and 8443/https. 16 | * By default, Spring Security will redirect 8080->8443 17 | */ 18 | @Configuration 19 | @Profile("ssl") 20 | public class SecureTomcatConfiguration { 21 | 22 | @Bean 23 | public EmbeddedServletContainerFactory servletContainer() throws FileNotFoundException { 24 | TomcatEmbeddedServletContainerFactory f = new TomcatEmbeddedServletContainerFactory(); 25 | f.addAdditionalTomcatConnectors(createSslConnector()); 26 | return f; 27 | } 28 | 29 | private Connector createSslConnector() throws FileNotFoundException { 30 | Connector connector = new Connector(Http11NioProtocol.class.getName()); 31 | Http11NioProtocol protocol = 32 | (Http11NioProtocol)connector.getProtocolHandler(); 33 | connector.setPort(8443); 34 | connector.setSecure(true); 35 | connector.setScheme("https"); 36 | protocol.setSSLEnabled(true); 37 | protocol.setKeyAlias("springagram"); 38 | protocol.setKeystorePass("password"); 39 | protocol.setKeystoreFile(ResourceUtils 40 | .getFile("src/main/resources/keystore.jks").getAbsolutePath()); 41 | protocol.setSslProtocol("TLS"); 42 | return connector; 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/SecurityDetailsLoader.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.annotation.PostConstruct; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | public class SecurityDetailsLoader { 13 | 14 | private final UserRepository userRepository; 15 | 16 | @Autowired 17 | public SecurityDetailsLoader(UserRepository userRepository) { 18 | this.userRepository = userRepository; 19 | } 20 | 21 | @PostConstruct 22 | public void init() throws IOException { 23 | 24 | User greg = new User(); 25 | greg.setName("greg"); 26 | greg.setPassword("turnquist"); 27 | greg.setRoles(new String[]{"ROLE_USER"}); 28 | userRepository.save(greg); 29 | 30 | User roy = new User(); 31 | roy.setName("roy"); 32 | roy.setPassword("clarkson"); 33 | roy.setRoles(new String[]{"ROLE_USER"}); 34 | userRepository.save(roy); 35 | 36 | SecurityContextHolder.clearContext(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/SpringDataJpaUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.core.authority.AuthorityUtils; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class SpringDataJpaUserDetailsService implements UserDetailsService { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(SpringDataJpaUserDetailsService.class); 17 | 18 | private UserRepository repository; 19 | 20 | @Autowired 21 | public SpringDataJpaUserDetailsService(UserRepository repository) { 22 | this.repository = repository; 23 | } 24 | 25 | @Override 26 | public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { 27 | log.info("Fetching user " + s); 28 | User user = repository.findByName(s); 29 | log.info("Transforming " + user + " into UserDetails object"); 30 | UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(), 31 | AuthorityUtils.createAuthorityList(user.getRoles())); 32 | log.info("About to return " + userDetails); 33 | return userDetails; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/TaskSchedulingConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.scheduling.TaskScheduler; 6 | import org.springframework.scheduling.annotation.SchedulingConfigurer; 7 | import org.springframework.scheduling.config.ScheduledTaskRegistrar; 8 | 9 | /** 10 | * Workaround multiple TaskScheduler beans by setting the default 11 | */ 12 | @Configuration 13 | public class TaskSchedulingConfiguration implements SchedulingConfigurer { 14 | 15 | @Autowired 16 | private TaskScheduler taskScheduler; 17 | 18 | @Override 19 | public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { 20 | taskRegistrar.setScheduler(this.taskScheduler); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/User.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.Id; 6 | 7 | import lombok.Data; 8 | import lombok.ToString; 9 | 10 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | 13 | import com.fasterxml.jackson.annotation.JsonIgnore; 14 | 15 | @Data 16 | @ToString(exclude = "password") 17 | @Entity 18 | public class User { 19 | 20 | public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); 21 | 22 | @Id @GeneratedValue 23 | private Long id; 24 | 25 | private String name; 26 | 27 | @JsonIgnore 28 | private String password; 29 | 30 | private String[] roles; 31 | 32 | public void setPassword(String password) { 33 | this.password = PASSWORD_ENCODER.encode(password); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import org.springframework.data.repository.Repository; 4 | 5 | /** 6 | * Repository to support Spring Security/Spring Data JPA 7 | */ 8 | public interface UserRepository extends Repository { 9 | 10 | User save(User user); 11 | 12 | User findByName(String name); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/WebConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 6 | 7 | @Configuration 8 | public class WebConfiguration extends WebMvcConfigurerAdapter { 9 | 10 | @Override 11 | public void addViewControllers(ViewControllerRegistry registry) { 12 | registry.addViewController("/login").setViewName("login"); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/WebSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.core.env.Environment; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 9 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 13 | import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableGlobalMethodSecurity(prePostEnabled = true) 18 | @EnableRedisHttpSession 19 | public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { 20 | 21 | @Autowired 22 | SpringDataJpaUserDetailsService userDetailsService; 23 | 24 | @Autowired 25 | Environment env; 26 | 27 | @Override 28 | public void configure(AuthenticationManagerBuilder auth) throws Exception { 29 | auth.userDetailsService(userDetailsService).passwordEncoder(User.PASSWORD_ENCODER); 30 | } 31 | 32 | // Needed by Spring Security OAuth 33 | @Override 34 | @Bean 35 | public AuthenticationManager authenticationManagerBean() throws Exception { 36 | return super.authenticationManagerBean(); 37 | } 38 | 39 | @Override 40 | protected void configure(HttpSecurity http) throws Exception { 41 | http 42 | .authorizeRequests() 43 | // NOTE: If you add other static resources to src/main/resources, they must be 44 | // listed here to avoid security checks 45 | .antMatchers("/bower_components/**", "/run.js", "/app/**", "/main.css", "/docs/**").permitAll() 46 | .anyRequest().authenticated() 47 | .and() 48 | .formLogin() 49 | .loginPage("/login") 50 | .permitAll() 51 | .and() 52 | .logout() 53 | .logoutSuccessUrl("/") 54 | .and() 55 | .csrf().disable(); 56 | 57 | if (env.acceptsProfiles("basic")) { 58 | http.httpBasic(); 59 | } 60 | 61 | if (env.acceptsProfiles("ssl")) { 62 | http.requiresChannel().anyRequest().requiresSecure(); 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/java/com/greglturnquist/springagram/frontend/WebSocketConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.frontend; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.session.ExpiringSession; 6 | import org.springframework.session.web.socket.config.annotation.AbstractSessionWebSocketMessageBrokerConfigurer; 7 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 8 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 9 | 10 | @Configuration 11 | @EnableWebSocketMessageBroker 12 | public class WebSocketConfiguration extends AbstractSessionWebSocketMessageBrokerConfigurer { 13 | 14 | public static final String MESSAGE_PREFIX = "/topic"; 15 | 16 | @Override 17 | public void configureMessageBroker(MessageBrokerRegistry registry) { 18 | registry.enableSimpleBroker(MESSAGE_PREFIX); 19 | registry.setApplicationDestinationPrefixes("/app"); 20 | } 21 | 22 | @Override 23 | protected void configureStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { 24 | stompEndpointRegistry.addEndpoint("/springagram").withSockJS(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: spring-a-gram-frontend 4 | data: 5 | rest: 6 | basePath: /api 7 | 8 | multipart: 9 | maxFileSize: 10Mb 10 | 11 | eureka: 12 | client: 13 | serviceUrl: 14 | defaultZone: ${eureka.address:localhost:8761}/eureka/ 15 | instance: 16 | leaseRenewalIntervalInSeconds: 5 17 | hostname: ${vcap.application.uris[0]:localhost} 18 | metadataMap: 19 | instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}} 20 | 21 | hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000 22 | ribbon: 23 | ConnectTimeout: 3000 24 | ReadTimeout: 60000 25 | 26 | zuul: 27 | routes: 28 | api: 29 | path: /api/** 30 | serviceId: spring-a-gram-backend 31 | stripPrefix: false 32 | files: 33 | path: /files/** 34 | serviceId: spring-a-gram-mongodb-fileservice 35 | stripPrefix: false 36 | 37 | logging: 38 | level: 39 | ROOT: INFO 40 | com.gregturnquist.springagram: DEBUG 41 | org.hibernate.SQL: DEBUG 42 | 43 | --- 44 | spring: 45 | profiles: s3 46 | 47 | zuul: 48 | routes: 49 | api: 50 | path: /api/** 51 | serviceId: spring-a-gram-backend 52 | stripPrefix: false 53 | files: 54 | path: /files/** 55 | serviceId: spring-a-gram-s3-fileservice 56 | stripPrefix: false 57 | 58 | --- 59 | spring: 60 | profiles: cloud 61 | 62 | eureka: 63 | instance: 64 | nonSecurePort: 80 65 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregturn/spring-a-gram/171f8675ed22308936e43eebe4f18ef6d90d9010/spring-a-gram-frontend/src/main/resources/cat.jpg -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/caterpillar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregturn/spring-a-gram/171f8675ed22308936e43eebe4f18ef6d90d9010/spring-a-gram-frontend/src/main/resources/caterpillar.jpg -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregturn/spring-a-gram/171f8675ed22308936e43eebe4f18ef6d90d9010/spring-a-gram-frontend/src/main/resources/keystore.jks -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/rest-messages.properties: -------------------------------------------------------------------------------- 1 | rest.description.item=An image 2 | rest.description.item.id=primary key used internally to store an item (not for RESTful usage) 3 | rest.description.item.image=An Base64-encoded version of the image 4 | rest.description.item.gallery=If populated, the gallery the image is linked to 5 | rest.description.item.htmlUrl=The URL to view the image on a web page (instead of the HATEOAS record) 6 | 7 | rest.description.gallery=A collection of images 8 | rest.description.gallery.id=primary key used internally to store a gallery (not for RESTful usage) 9 | rest.description.gallery.description=Description of the entire gallery 10 | rest.description.gallery.items=Links to images associated with this gallery 11 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/api.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var rest = require('rest'); 5 | var defaultRequest = require('rest/interceptor/defaultRequest'); 6 | var mime = require('rest/interceptor/mime'); 7 | var uriTemplateInterceptor = require('./api/uriTemplateInterceptor'); 8 | var errorCode = require('rest/interceptor/errorCode'); 9 | var baseRegistry = require('rest/mime/registry'); 10 | 11 | var registry = baseRegistry.child(); 12 | 13 | registry.register('text/uri-list', require('./api/uriListConverter')); 14 | registry.register('application/hal+json', require('rest/mime/type/application/hal')); 15 | 16 | return rest 17 | .wrap(mime, { registry: registry }) 18 | .wrap(uriTemplateInterceptor) 19 | .wrap(errorCode) 20 | .wrap(defaultRequest, { headers: { 'Accept': 'application/hal+json' }}); 21 | 22 | }); -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/api/uriListConverter.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 'use strict'; 3 | 4 | /* Convert a single or array of resources into "URI1\nURI2\nURI3..." */ 5 | return { 6 | read: function(str /*, opts */) { 7 | return str.split('\n'); 8 | }, 9 | write: function(obj /*, opts */) { 10 | // If this is an Array, extract the self URI and then join using a newline 11 | if (obj instanceof Array) { 12 | return obj.map(function(resource) { 13 | return resource._links.self.href; 14 | }).join('\n'); 15 | } else { // otherwise, just return the self URI 16 | return obj._links.self.href; 17 | } 18 | } 19 | }; 20 | 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/api/uriTemplateInterceptor.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var interceptor = require('rest/interceptor'); 5 | 6 | return interceptor({ 7 | request: function (request /*, config, meta */) { 8 | /* If the URI is a URI Template per RFC 6570 (http://tools.ietf.org/html/rfc6570), trim out the template part */ 9 | if (request.path.indexOf('{') === -1) { 10 | return request; 11 | } else { 12 | request.path = request.path.split('{')[0]; 13 | return request; 14 | } 15 | } 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/follow.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 3 | return function follow(api, rootPath, relArray) { 4 | var root = api({ 5 | method: 'GET', 6 | path: rootPath 7 | }); 8 | 9 | return relArray.reduce(function(root, arrayItem) { 10 | var rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel; 11 | return traverseNext(root, rel, arrayItem); 12 | }, root); 13 | 14 | function traverseNext (root, rel, arrayItem) { 15 | return root.then(function (response) { 16 | if (hasEmbeddedRel(response.entity, rel)) { 17 | return response.entity._embedded[rel]; 18 | } 19 | 20 | if(!response.entity._links) { 21 | return []; 22 | } 23 | 24 | if (typeof arrayItem === 'string') { 25 | return api({ 26 | method: 'GET', 27 | path: response.entity._links[rel].href 28 | }); 29 | } else { 30 | return api({ 31 | method: 'GET', 32 | path: response.entity._links[rel].href, 33 | params: arrayItem.params 34 | }); 35 | } 36 | }); 37 | } 38 | 39 | function hasEmbeddedRel (entity, rel) { 40 | return entity._embedded && entity._embedded.hasOwnProperty(rel); 41 | } 42 | }; 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/hateoasHelper.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | 3 | return { 4 | wrapSelfHref: wrapSelfHref 5 | }; 6 | 7 | // Taken an href and serve it up via obj._links.self.href 8 | function wrapSelfHref(href) { 9 | return { 10 | _links: { 11 | self: { 12 | href: href 13 | } 14 | } 15 | } 16 | } 17 | 18 | 19 | }); -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/images.jsx: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | 'use strict'; 3 | 4 | require('./polyfill'); 5 | 6 | var React = require('react'); 7 | var client = require('./api'); 8 | var stompClient = require('./websocket-listener'); 9 | var _ = require('lodash'); 10 | var twitter = require('./twitter'); 11 | var linkHelper = require('./linkHelper'); 12 | var hateoasHelper = require('./hateoasHelper'); 13 | var when = require('when'); 14 | var follow = require('./follow'); 15 | 16 | var talk = "https://2015.event.springone2gx.com/schedule/sessions/spring_data_rest_data_meets_hypermedia_security.html on 9/15 @ 12:45pm"; 17 | var tags = ['s2gx', 'REST']; 18 | 19 | var root = '/api'; 20 | 21 | var FileForm = require('jsx!app/upload'); 22 | var spinner = require('app/spinner'); 23 | 24 | var ItemContainer = React.createClass({ 25 | 26 | /** 27 | * Load the initial state from the server. 28 | */ 29 | 30 | loadItemsFromServer: function () { 31 | follow(client, root, [ 32 | 'items', 33 | 'search', 34 | { rel: 'findByGalleryIsNull', params: { projection: 'owner'}} 35 | ]).done(response => { 36 | this.setState({ 37 | data: response.entity._embedded.items, galleries: this.state.galleries, 38 | selectedGallery: this.state.selectedGallery 39 | }); 40 | }, response => { 41 | this.fallbackDataLoad(); 42 | }); 43 | }, 44 | loadGalleriesFromServer: function () { 45 | follow(client, root, ['galleries']).done(galleries => { 46 | when.all(galleries.entity._embedded.galleries.map(this.loadItemsForGallery)).done(galleries => { 47 | this.setState({ 48 | data: this.state.data, 49 | galleries: galleries, 50 | selectedGallery: this.state.selectedGallery 51 | }); 52 | }); 53 | }, 54 | response => { 55 | this.fallbackDataLoad(); 56 | }); 57 | }, 58 | loadItemsForGallery: function (gallery) { 59 | return client({ 60 | method: 'GET', 61 | path: gallery._links.items.href, 62 | params: {projection: 'owner'} 63 | }).then(items => { 64 | return { 65 | gallery: gallery, 66 | items: items.entity._embedded.items 67 | }; 68 | }) 69 | }, 70 | fallbackDataLoad: function() { 71 | this.setState({ 72 | data: [{ 73 | "image": "http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/images/logo.png", 74 | "user": {"name": "Spring-a-Gram"}, 75 | "_links": { 76 | "self": { 77 | "href": "http://localhost:8080/api/items/1" 78 | }, 79 | "item": { 80 | "href": "http://localhost:8080/api/items/1{?projection}", 81 | "templated": true 82 | }, 83 | "gallery": { 84 | "href": "http://localhost:8080/api/items/1/gallery" 85 | } 86 | } 87 | }], 88 | galleries: [], 89 | selectedGallery: undefined 90 | }); 91 | }, 92 | registerTopics: function () { 93 | stompClient.register([ 94 | {route: '/topic/backend.newItem', callback: this.addItemToUnlinkedList}, 95 | {route: '/topic/backend.deleteItem', callback: this.removeItemFromUnlinkedList}, 96 | {route: '/topic/backend.removeItemFromGallery-item', callback: this.addItemToUnlinkedList}, 97 | {route: '/topic/backend.removeItemFromGallery-gallery', callback: this.updateGallery}, 98 | {route: '/topic/backend.addItemToGallery-item', callback: this.removeItemFromUnlinkedList}, 99 | {route: '/topic/backend.addItemToGallery-gallery', callback: this.updateGallery} 100 | ]); 101 | this.props.spinner.hideSpinner() 102 | }, 103 | 104 | /** 105 | * User-clicked callbacks 106 | * 107 | * These implement user requests. They aren't responsible for updating the state of the UI. Instead, when 108 | * the operation completes on the server, a WebSocket message is expected. The handlers for those messages 109 | * instead will update the UI's state. 110 | */ 111 | 112 | onSelectGallery: function (gallery) { 113 | this.setState({data: this.state.data, galleries: this.state.galleries, selectedGallery: gallery}); 114 | }, 115 | onAddToGallery: function (item) { 116 | if (this.state.selectedGallery === undefined) { 117 | alert("You haven't selected a gallery yet"); 118 | return; 119 | } 120 | 121 | client({ 122 | method: 'PUT', 123 | path: item._links.gallery.href, 124 | entity: this.state.selectedGallery, 125 | headers: {'Content-Type': 'text/uri-list'} 126 | }).done(() => {/* Let the websocket handler update the state */}, 127 | response => { 128 | if (response.status.code === 403) { 129 | alert('You are not authorized to assign that picture to a gallery'); 130 | } 131 | } 132 | ); 133 | }, 134 | onRemoveFromGallery: function (item) { 135 | client({ 136 | method: 'DELETE', 137 | path: item._links.gallery.href 138 | }).done(() => {/* Let the websocket handler update the state */}, 139 | response => { 140 | if (response.status.code === 403) { 141 | alert('You are not authorized to assign that picture to a gallery'); 142 | } 143 | } 144 | ); 145 | }, 146 | onDelete: function (item) { 147 | client({ 148 | method: 'DELETE', 149 | path: item.image 150 | }).then(response => 151 | client({ 152 | method: 'DELETE', 153 | path: item._links.self.href.split('{')[0] 154 | }) 155 | ).done(() => {/* Let the websocket handler update the state */}, 156 | response => { 157 | if (response.status.code === 403) { 158 | alert('You are not authorized to delete that picture'); 159 | } 160 | }); 161 | }, 162 | 163 | /** 164 | * WebSocket-driven event handlers 165 | * 166 | * These handlers update the state of items and galleries. This way, the state is managed in one place, for 167 | * both the screen that made the update and all other clients. 168 | */ 169 | 170 | addItemToUnlinkedList: function (message) { 171 | client({ 172 | method: 'GET', 173 | path: message.body, 174 | params: {projection: "owner"} 175 | }).done(response => { 176 | this.setState({ 177 | data: this.state.data.concat([response.entity]), galleries: this.state.galleries, 178 | selectedGallery: this.state.selectedGallery 179 | }); 180 | }) 181 | }, 182 | removeItemFromUnlinkedList: function (message) { 183 | var items = this.state.data; 184 | _.remove(items, item => item._links.self.href.split('{')[0].endsWith(message.body)); 185 | this.setState({data: items, galleries: this.state.galleries, selectedGallery: this.state.selectedGallery}); 186 | }, 187 | updateGallery: function (message) { 188 | client({ 189 | method: 'GET', 190 | path: message.body 191 | }).then(response => { 192 | this.loadItemsForGallery(response.entity).done(refreshedGallery => { 193 | var newGalleries = this.state.galleries.map(gallery => { 194 | if (gallery.gallery._links.self.href === response.entity._links.self.href) { 195 | return refreshedGallery; 196 | } else { 197 | return gallery; 198 | } 199 | }); 200 | this.setState({ 201 | data: this.state.data, 202 | galleries: newGalleries, 203 | selectedGallery: this.state.selectedGallery 204 | }); 205 | }) 206 | }) 207 | }, 208 | 209 | /** 210 | * ReactJS's standard API hooks. 211 | */ 212 | 213 | getInitialState: function () { 214 | return {data: [], galleries: [], selectedGallery: undefined} 215 | }, 216 | 217 | componentDidMount: function () { 218 | when.try(this.loadItemsFromServer); 219 | when.try(this.loadGalleriesFromServer); 220 | when.try(this.registerTopics); 221 | }, 222 | 223 | render: function () { 224 | return ( 225 |
    226 |
    227 |

    Galleries

    228 | 229 | 231 |
    232 | 233 |
    234 |
    235 |
    236 | 237 |
    238 |

    Unlinked Images

    239 | 240 | 241 |
    242 |
    243 | ) 244 | } 245 | }); 246 | 247 | var GalleryList = React.createClass({ 248 | render: function () { 249 | var galleries = this.props.galleries.map(gallery => 250 | 254 | ); 255 | return ( 256 |
      257 | {galleries} 258 |
    259 | ) 260 | } 261 | }); 262 | 263 | var Gallery = React.createClass({ 264 | handleChange: function (e) { 265 | this.props.onSelectGallery(this.props.gallery); 266 | }, 267 | render: function () { 268 | var items = this.props.items.map(item => 269 | 272 | ); 273 | return ( 274 |
  • 275 | 276 | {this.props.gallery.description} 277 |
      278 | {items} 279 |
    280 |
  • 281 | ) 282 | } 283 | }); 284 | 285 | var ItemInGallery = React.createClass({ 286 | handleRemove: function () { 287 | this.props.onRemoveFromGallery(this.props.item); 288 | }, 289 | render: function () { 290 | return ( 291 |
  • 292 |
    293 | 294 | 295 |
    296 |
    297 | Tweet 299 | 302 | 303 | 304 | 305 | {this.props.item.user.name} 306 |
    307 |
    308 |
    309 |
  • 310 | ) 311 | } 312 | }); 313 | 314 | var ItemList = React.createClass({ 315 | render: function () { 316 | var items = this.props.data.map(item => 317 | 319 | ); 320 | return ( 321 |
      322 | {items} 323 |
    324 | ) 325 | } 326 | }); 327 | 328 | var Item = React.createClass({ 329 | handleAddToGallery: function () { 330 | this.props.onAddToGallery(this.props.item); 331 | }, 332 | handleDelete: function () { 333 | this.props.onDelete(this.props.item); 334 | }, 335 | render: function () { 336 | return ( 337 |
  • 338 |
    339 | 340 | 341 |
    342 |
    343 | Tweet 345 | 348 | 351 | 352 | 353 | 354 | Uploaded by: {this.props.item.user.name} 355 |
    356 |
    357 |
    358 |
  • 359 | ) 360 | } 361 | }) 362 | 363 | React.render( 364 | , 365 | document.getElementById('upload') 366 | ); 367 | 368 | React.render( 369 | , 370 | document.getElementById('react') 371 | ); 372 | 373 | }); -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/linkHelper.js: -------------------------------------------------------------------------------- 1 | (function(document) { 2 | define(function(require) { 3 | 'use strict'; 4 | 5 | return { 6 | htmlUrl: htmlUrl 7 | }; 8 | 9 | function htmlUrl(uri) { 10 | var encodedUri = encodeURIComponent(uri.split('{')[0]); 11 | var link = window.location.origin + '/image?link=' + encodedUri; 12 | return link; 13 | } 14 | 15 | }); 16 | }(document)); 17 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/main.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | document.addEventListener('DOMContentLoaded', new function() { 5 | 6 | require('jsx!app/images'); 7 | 8 | }, false); 9 | 10 | }); 11 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/polyfill.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | 'use strict'; 3 | 4 | /** 5 | * Collection of hand-written polyfills, because the best source I found required bundling to support 6 | * requireJS 7 | */ 8 | 9 | /** 10 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith 11 | */ 12 | if (!String.prototype.endsWith) { 13 | String.prototype.endsWith = function(searchString, position) { 14 | var subjectString = this.toString(); 15 | if (position === undefined || position > subjectString.length) { 16 | position = subjectString.length; 17 | } 18 | position -= searchString.length; 19 | var lastIndex = subjectString.indexOf(searchString, position); 20 | return lastIndex !== -1 && lastIndex === position; 21 | }; 22 | } 23 | 24 | }); -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/spinner.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | 'use strict'; 3 | 4 | return { 5 | showSpinner: function () { 6 | document.getElementById('spinner').style.cssText = ''; 7 | }, 8 | 9 | hideSpinner: function () { 10 | document.getElementById('spinner').style.cssText = 'display: none;'; 11 | } 12 | } 13 | 14 | }); -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/twitter.js: -------------------------------------------------------------------------------- 1 | (function(document) { 2 | define(function(require) { 3 | 'use strict'; 4 | 5 | var linkHelper = require('./linkHelper'); 6 | 7 | return { 8 | tweetIntentWithHref: tweetIntentWithHref, 9 | tweetIntent: tweetIntent 10 | }; 11 | 12 | function tweetIntentWithHref(href, talk, tags) { 13 | return "https://twitter.com/intent/tweet?text=" + 14 | encodeURIComponent(href + " was uploaded by Spring-a-Gram. ") + 15 | "&hashtags=" + tags.join(','); 16 | } 17 | 18 | function tweetIntent(item, talk, tags) { 19 | return tweetIntentWithHref(linkHelper.htmlUrl(item._links.self.href), talk, tags); 20 | } 21 | 22 | }); 23 | }(document)); 24 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/upload.jsx: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | 'use strict'; 3 | 4 | var React = require('react'); 5 | var client = require('./api'); 6 | var when = require('when'); 7 | var follow = require('./follow'); 8 | 9 | var root = '/api'; 10 | 11 | var FileForm = React.createClass({ 12 | handleSubmit: function (e) { 13 | e.preventDefault(); 14 | 15 | this.props.spinner.showSpinner(); 16 | when.promise((resolve, reject) => { 17 | var request = new XMLHttpRequest(); 18 | 19 | request.onerror = reject; 20 | request.onload = function () { 21 | resolve(request); 22 | } 23 | 24 | var selectedFile = React.findDOMNode(this.refs.fileInput).files[0]; 25 | 26 | var formData = new FormData(); 27 | formData.append("name", selectedFile.name + Date.now()); 28 | formData.append("file", selectedFile); 29 | 30 | request.open('POST', '/files'); 31 | request.send(formData); 32 | }).then(response => 33 | response.getResponseHeader("Location") 34 | ).then(location => { 35 | if (location === null) { 36 | return "No location header"; 37 | } 38 | return follow(client, root, ['items']).then(function(response) { 39 | return client({ 40 | method: 'POST', 41 | path: response.entity._links.self.href, 42 | entity: {image: location}, 43 | headers: {'Content-Type': 'application/json'} 44 | }); 45 | }); 46 | }).done(response => { 47 | React.findDOMNode(this.refs.fileInput).value = null; 48 | this.props.spinner.hideSpinner(); 49 | }); 50 | }, 51 | render: function () { 52 | return ( 53 |
    54 |

    55 |

    56 |
    57 | ) 58 | } 59 | }); 60 | 61 | return FileForm; 62 | 63 | }); -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/app/websocket-listener.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var sockjs = require('sockjs-client'); 5 | var stomp = require('stomp-websocket'); 6 | 7 | return { 8 | register: register 9 | }; 10 | 11 | function register(registrations) { 12 | var socket = new SockJS('/springagram'); 13 | var stompClient = Stomp.over(socket); 14 | stompClient.connect({}, function(frame) { 15 | console.log('Connected: ' + frame); 16 | registrations.forEach(function (registration) { 17 | stompClient.subscribe(registration.route, registration.callback); 18 | }); 19 | }); 20 | } 21 | 22 | }); -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-a-gram", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/gregturn/spring-a-gram", 5 | "authors": [ 6 | "Greg Turnquist " 7 | ], 8 | "description": "Upload your fav pics", 9 | "main": "app/main.js", 10 | "moduleType": [ 11 | "amd" 12 | ], 13 | "keywords": [ 14 | "rest", 15 | "hateoas", 16 | "spring", 17 | "data" 18 | ], 19 | "license": "ASL", 20 | "private": true, 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests" 27 | ], 28 | "dependencies": { 29 | "inuit-starter-kit": "~0.2.8", 30 | "inuit-layout": "~0.3.2", 31 | "inuit-widths-responsive": "~0.1.3", 32 | "inuit-media": "~0.4.2", 33 | "inuit-buttons": "~0.4.1", 34 | "inuit-box": "~0.4.4", 35 | "rest": "~1.3.1", 36 | "requirejs": "~2.1.15", 37 | "sockjs-client": "~0.3.4", 38 | "stomp-websocket": "~2.3.4", 39 | "jsx-requirejs-plugin": "~0.6.2", 40 | "lodash": "~3.10.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/main.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // Pre-sets must be done BEFORE a module is imported. Some of these settings are at the top, while others 3 | // are right before the import statement. 4 | //////////////////////////////////////////////////////////////////////// 5 | 6 | // Spring's palette (found on spring.io) 7 | $spring-green: #6db33f; 8 | $spring-black: #34302d; 9 | $spring-antique-white: #F1F1F1; 10 | $spring-white: #eee; 11 | 12 | //$inuit-base-font-size: 12px; 13 | $inuit-base-line-height: 18px; 14 | $inuit-base-background-color: $spring-antique-white; 15 | 16 | //////////////////////////////////////////////////////////////////////// 17 | // Here are the layers of inuit. Each layer needs to have its order preserved, but modules within the same layer 18 | // can be imported in any order. 19 | //////////////////////////////////////////////////////////////////////// 20 | 21 | // Settings layer 22 | @import "bower_components/inuit-defaults/settings.defaults"; 23 | @import "bower_components/inuit-responsive-settings/settings.responsive"; 24 | 25 | // Tools layer 26 | @import "bower_components/inuit-functions/tools.functions"; 27 | @import "bower_components/inuit-mixins/tools.mixins"; 28 | @import "bower_components/inuit-responsive-tools/tools.responsive"; 29 | 30 | // Generic layer 31 | @import "bower_components/inuit-normalize/generic.normalize"; 32 | @import "bower_components/inuit-box-sizing/generic.box-sizing"; 33 | 34 | @import "bower_components/inuit-page/base.page"; 35 | 36 | // Objects layer 37 | $inuit-enable-layout--large: true; 38 | @import "bower_components/inuit-layout/objects.layout"; 39 | 40 | $inuit-enable-media--large: true; 41 | $inuit-enable-media--responsive: true; 42 | @import "bower_components/inuit-media/objects.media"; 43 | 44 | //$inuit-enable-btn--full: true; 45 | $inuit-enable-btn--small: true; 46 | $inuit-btn-background: $spring-green; 47 | $inuit-btn-color: $spring-white; 48 | @import "bower_components/inuit-buttons/objects.buttons"; 49 | 50 | $inuit-enable-box--tiny: true; 51 | @import "bower_components/inuit-box/objects.box"; 52 | 53 | // Trumps layer 54 | @import "bower_components/inuit-widths/trumps.widths"; 55 | @import "bower_components/inuit-clearfix/trumps.clearfix"; 56 | @import "bower_components/inuit-widths-responsive/trumps.widths-responsive"; 57 | 58 | //////////////////////////////////////////////////////////////////////// 59 | // Custom CSS additions and overrides 60 | // 61 | // This isn't to override settings from inuit, but instead, to override or add to CSS generated 62 | // by inuit. 63 | //////////////////////////////////////////////////////////////////////// 64 | 65 | // Plugin the Ubuntu font 66 | html { 67 | font-family: 'Ubuntu', sans-serif; 68 | } 69 | 70 | // Dress up the top row of a "page" 71 | // Add a small amount of padding at the bottom of a "page" 72 | $inuit-base-spacing-unit--tiny: round($inuit-base-spacing-unit / 8); 73 | .page { 74 | border-top: $spring-green solid 4px; 75 | color: $spring-white; 76 | background-color: $spring-black; 77 | width: 100%; 78 | margin-top: 0em; 79 | padding-bottom: $inuit-base-spacing-unit--tiny; 80 | } 81 | 82 | // Wrap a given block with insets so it isn't pushed to the edge of the browser 83 | .wrapper { 84 | max-width: 1100px; 85 | margin: 0 auto; 86 | padding-left: 12px; 87 | padding-right: 12px; 88 | } 89 | 90 | // Fine tune the bar at the top, so it shrinks vertically on portable devices 91 | h1 { 92 | @include media-query(portable) { 93 | margin: 0.1em 0; 94 | } 95 | } 96 | 97 | //////////////////////////////////////////////////////////////////////// 98 | // Experimental CSS spinner - Based on http://codepen.io/adonisk/pen/IAzbo/ 99 | // Removed for now 100 | //////////////////////////////////////////////////////////////////////// 101 | 102 | // TODO: Write explanatory comment (can't remember why I made this right now) 103 | .border { 104 | border: 1px solid; 105 | } 106 | 107 | // Extend btn so that the button is either a regular button or a small button, based on a media query. 108 | .btn--responsive { 109 | @extend %btn; 110 | @include media-query(portable) { 111 | padding: $inuit-btn-padding--small - $inuit-btn-border-width double($inuit-btn-padding--small) - $inuit-btn-border-width; /* [7] */ 112 | } 113 | } 114 | 115 | .login-page .layout { 116 | margin-top: $inuit-base-line-height; 117 | margin-bottom: $inuit-base-line-height; 118 | } 119 | 120 | 121 | //////////////////////////////////////////////////////////////////////// 122 | // CSS Spinner 123 | //////////////////////////////////////////////////////////////////////// 124 | //$spinner5: #ffffff; 125 | $spinner5: $spring-green; 126 | $spinner6: rgba(255, 255, 255, 0); 127 | $spinner7: $spring-antique-white; 128 | 129 | .loader { 130 | font-size: 20px; 131 | margin: 50px auto; 132 | text-indent: -9999em; 133 | width: 11em; 134 | height: 11em; 135 | border-radius: 50%; 136 | background: $spinner5; 137 | background: -moz-linear-gradient(left, $spinner5 10%, $spinner6 42%); 138 | background: -webkit-linear-gradient(left, $spinner5 10%, $spinner6 42%); 139 | background: -o-linear-gradient(left, $spinner5 10%, $spinner6 42%); 140 | background: -ms-linear-gradient(left, $spinner5 10%, $spinner6 42%); 141 | background: linear-gradient(to right, $spinner5 10%, $spinner6 42%); 142 | position: absolute; 143 | top: 25%; 144 | left: 45%; 145 | -webkit-animation: load3 1.4s infinite linear; 146 | animation: load3 1.4s infinite linear; 147 | -webkit-transform: translateZ(0); 148 | -ms-transform: translateZ(0); 149 | transform: translateZ(0); 150 | } 151 | 152 | .loader--responsive { 153 | @include media-query(portable) { 154 | left: 15%; 155 | } 156 | } 157 | 158 | .loader:before { 159 | width: 50%; 160 | height: 50%; 161 | background: $spinner5; 162 | border-radius: 100% 0 0 0; 163 | position: absolute; 164 | top: 0; 165 | left: 0; 166 | content: ''; 167 | } 168 | .loader:after { 169 | background: $spinner7; 170 | width: 75%; 171 | height: 75%; 172 | border-radius: 50%; 173 | content: ''; 174 | margin: auto; 175 | position: absolute; 176 | top: 0; 177 | left: 0; 178 | bottom: 0; 179 | right: 0; 180 | } 181 | @-webkit-keyframes load3 { 182 | 0% { 183 | -webkit-transform: rotate(0deg); 184 | transform: rotate(0deg); 185 | } 186 | 100% { 187 | -webkit-transform: rotate(360deg); 188 | transform: rotate(360deg); 189 | } 190 | } 191 | @keyframes load3 { 192 | 0% { 193 | -webkit-transform: rotate(0deg); 194 | transform: rotate(0deg); 195 | } 196 | 100% { 197 | -webkit-transform: rotate(360deg); 198 | transform: rotate(360deg); 199 | } 200 | } -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spring-a-Gram", 3 | "version": "1.0.0", 4 | "description": "Demo Spring Data REST by uploading your favorite pics", 5 | "main": "run.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:gregturn/spring-a-gram.git" 9 | }, 10 | "keywords": [ 11 | "spring", 12 | "data", 13 | "rest", 14 | "hateoas", 15 | "hypermedia" 16 | ], 17 | "author": "Greg L. Turnquist", 18 | "license": "ASLv2", 19 | "bugs": { 20 | "url": "https://github.com/gregturn/spring-a-gram/issues" 21 | }, 22 | "homepage": "https://github.com/gregturn/spring-a-gram", 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "bower": "^1.3.12" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/static/run.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | requirejs.config({ 4 | packages: [ 5 | {name: 'rest', location: 'bower_components/rest', main: 'browser'}, 6 | {name: 'when', location: 'bower_components/when', main: 'when'}, 7 | {name: 'sockjs-client', location: 'bower_components/sockjs-client', main: 'dist/sockjs'}, 8 | {name: 'stomp-websocket', location: 'bower_components/stomp-websocket', main: 'lib/stomp.min'}, 9 | {name: 'react', location: 'bower_components/react', main: 'react'}, 10 | {name: 'JSXTransformer', location: 'bower_components/jsx-requirejs-plugin/js', main: 'JSXTransformer'}, 11 | {name: 'jsx', location: 'bower_components/jsx-requirejs-plugin/js', main: 'jsx'}, 12 | {name: 'text', location: 'bower_components/requirejs-text', main: 'text'}, 13 | {name: 'lodash', location: 'bower_components/lodash', main: 'lodash.min'} 14 | ], 15 | deps: ['app/main'], 16 | jsx: { 17 | fileExtension: ".jsx", 18 | harmony: true, 19 | stripTypes: true 20 | } 21 | }); 22 | 23 | }()); -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/templates/_links.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |

    Links:

    5 | 10 |
    11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spring-a-Gram :: Login 6 | 7 | 8 | 9 | 10 | 11 |
    12 |

    Spring-a-Gram

    13 |
    14 | 15 |
    16 |

    17 | Uh oh! Something went wrong with your billion dollar idea! 18 |

    19 |
    20 | 21 |
    22 |
    23 | 24 | 25 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spring-a-Gram :: Upload your fav pics 6 | 7 | 8 | 9 | 10 | 11 | 12 |
    13 |

    Spring-a-Gram

    14 |
    15 | 16 |
    17 | 18 | Hello, user. 19 |
    20 | 21 |
    22 | 23 |
    24 | 25 |
    Loading...
    26 | 27 | 28 |
    29 |

    Snap a pic

    30 |
    31 |
    32 | 33 |
    34 |

    Add a gallery

    35 | 36 |
    37 | 38 | 39 |
    40 |
    41 | 42 |
    43 | 44 |
    45 |
    46 |
    47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spring-a-Gram :: Login 6 | 7 | 8 | 9 | 10 | 11 |
    12 |

    Spring-a-Gram

    13 |
    14 |
    15 |
    16 | Invalid username and password. 17 |
    18 |
    19 | You have been logged out. 20 |
    21 | 32 |
    33 |

    Try one of these accounts:

    34 |
      35 |
    • roy/clarkson
    • 36 |
    • greg/turnquist
    • 37 |
    38 |
    39 |
    40 | 41 | -------------------------------------------------------------------------------- /spring-a-gram-frontend/src/main/resources/templates/oneImage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spring-A-Gram :: Upload your fav pics 6 | 7 | 8 | 9 | 10 | 11 |
    12 |

    Spring-a-Gram

    13 |
    14 | 15 |
    16 |

    Single Image

    17 |
    18 | 19 |
    20 |
    21 | 22 |
    23 | Tweet me! 24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /spring-a-gram-hystrix-dashboard/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.royclarkson 7 | spring-a-gram-hystrix-dashboard 8 | 0.1.0 9 | 10 | 11 | com.greglturnquist.springagram 12 | spring-a-gram 13 | 0.1.0 14 | 15 | 16 | 17 | 18 | org.springframework.cloud 19 | spring-cloud-starter-hystrix-dashboard 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /spring-a-gram-hystrix-dashboard/src/main/java/com/royclarkson/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.royclarkson; 17 | 18 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 19 | import org.springframework.boot.builder.SpringApplicationBuilder; 20 | import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; 21 | import org.springframework.context.annotation.Configuration; 22 | 23 | @EnableAutoConfiguration 24 | @EnableHystrixDashboard 25 | public class Application { 26 | 27 | public static void main(String[] args) { 28 | new SpringApplicationBuilder(Application.class).web(true).run(args); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /spring-a-gram-hystrix-dashboard/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | info: 2 | component: Hystrix Dashboard 3 | 4 | server: 5 | port: 7979 6 | 7 | hystrix: 8 | dashboard: 9 | enableIgnoreConnectionCloseHeader: true 10 | 11 | logging: 12 | level: 13 | ROOT: INFO 14 | org.springframework.cloud: DEBUG 15 | 16 | --- 17 | spring: 18 | profiles: cloud 19 | 20 | eureka: 21 | instance: 22 | nonSecurePort: 80 23 | client: 24 | serviceUrl: 25 | defaultZone: http://spring-a-gram-eureka-server.cfapps.io/eureka/ 26 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.greglturnquist.springagram 9 | spring-a-gram 10 | 0.1.0 11 | 12 | 13 | com.greglturnquist.springagram 14 | spring-a-gram-mongodb-fileservice 15 | 0.1.0 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-data-mongodb 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-data-rest 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | org.springframework.hateoas 32 | spring-hateoas 33 | 34 | 35 | io.pivotal.spring.cloud 36 | spring-cloud-services-starter-service-registry 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-security 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/ApplicationController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.fileservice.mongodb; 17 | 18 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; 19 | 20 | import java.io.IOException; 21 | import java.net.URI; 22 | import java.net.URISyntaxException; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import org.springframework.beans.factory.annotation.Autowired; 28 | import org.springframework.context.annotation.Configuration; 29 | import org.springframework.core.io.InputStreamResource; 30 | import org.springframework.data.mongodb.gridfs.GridFsResource; 31 | import org.springframework.hateoas.Link; 32 | import org.springframework.hateoas.ResourceSupport; 33 | import org.springframework.http.HttpStatus; 34 | import org.springframework.http.MediaType; 35 | import org.springframework.http.ResponseEntity; 36 | import org.springframework.web.bind.annotation.PathVariable; 37 | import org.springframework.web.bind.annotation.RequestMapping; 38 | import org.springframework.web.bind.annotation.RequestMethod; 39 | import org.springframework.web.bind.annotation.RequestParam; 40 | import org.springframework.web.bind.annotation.RestController; 41 | import org.springframework.web.multipart.MultipartFile; 42 | import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; 43 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 44 | 45 | /** 46 | * @author Greg Turnquist 47 | */ 48 | @RestController 49 | public class ApplicationController { 50 | 51 | private static final Logger log = LoggerFactory.getLogger(ApplicationController.class); 52 | 53 | private final FileService fileService; 54 | 55 | @Autowired 56 | public ApplicationController(FileService fileService) { 57 | this.fileService = fileService; 58 | } 59 | 60 | @RequestMapping(method = RequestMethod.POST, value = "/files") 61 | public ResponseEntity newFile(@RequestParam("name") String filename, @RequestParam("file") MultipartFile file) { 62 | 63 | if (!file.isEmpty()) { 64 | try { 65 | this.fileService.saveFile(file.getInputStream(), filename); 66 | 67 | Link link = linkTo(methodOn(ApplicationController.class).getFile(filename)).withRel(filename); 68 | return ResponseEntity.created(new URI(link.getHref())).build(); 69 | 70 | } catch (IOException | URISyntaxException e) { 71 | return ResponseEntity.badRequest().body("Couldn't process the request"); 72 | } 73 | } else { 74 | return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body("File is empty"); 75 | } 76 | } 77 | 78 | @RequestMapping(method = RequestMethod.GET, value = "/files") 79 | public ResponseEntity listAllFiles() { 80 | 81 | ResourceSupport files = new ResourceSupport(); 82 | 83 | for (GridFsResource resource : this.fileService.findAll()) { 84 | files.add(linkTo(methodOn(ApplicationController.class).getFile(resource.getFilename())) 85 | .withRel(resource.getFilename())); 86 | } 87 | 88 | return ResponseEntity.ok(files); 89 | } 90 | 91 | @RequestMapping(method = RequestMethod.GET, value = "/files/{filename}") 92 | public ResponseEntity getFile(@PathVariable String filename) { 93 | 94 | GridFsResource file = this.fileService.findOne(filename); 95 | 96 | if (file == null) { 97 | return ResponseEntity.notFound().build(); 98 | } 99 | 100 | try { 101 | return ResponseEntity.ok().contentLength(file.contentLength()) 102 | .contentType(MediaType.parseMediaType(file.getContentType())) 103 | .body(new InputStreamResource(file.getInputStream())); 104 | } 105 | catch (IOException e) { 106 | return ResponseEntity.badRequest().body("Couldn't process the request"); 107 | } 108 | } 109 | 110 | @RequestMapping(method = RequestMethod.DELETE, value = "/files/{filename}") 111 | public ResponseEntity deleteFile(@PathVariable String filename) { 112 | 113 | this.fileService.deleteOne(filename); 114 | 115 | return ResponseEntity.noContent().build(); 116 | } 117 | 118 | /** 119 | * Suffixes like ".jpg" should be part of the path and not extracted for content negotiation. 120 | */ 121 | @Configuration 122 | static class AllResources extends WebMvcConfigurerAdapter { 123 | 124 | @Override 125 | public void configurePathMatch(PathMatchConfigurer matcher) { 126 | matcher.setUseSuffixPatternMatch(false); 127 | } 128 | 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/DatabaseLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.fileservice.mongodb; 17 | 18 | import java.io.IOException; 19 | 20 | import javax.annotation.PostConstruct; 21 | 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.context.ApplicationContext; 24 | import org.springframework.context.annotation.Profile; 25 | import org.springframework.stereotype.Service; 26 | 27 | /** 28 | * @author Greg Turnquist 29 | */ 30 | @Service 31 | @Profile("!production") 32 | public class DatabaseLoader { 33 | 34 | private final FileService fileService; 35 | private final ApplicationContext ctx; 36 | 37 | @Autowired 38 | public DatabaseLoader(UserRepository userRepository, FileService fileService, ApplicationContext ctx) { 39 | this.fileService = fileService; 40 | this.ctx = ctx; 41 | } 42 | 43 | @PostConstruct 44 | public void init() throws IOException { 45 | 46 | this.fileService.deleteAll(); 47 | // loadImage("caterpillar.jpg"); 48 | } 49 | 50 | private void loadImage(String filename) throws IOException { 51 | this.fileService.saveFile(ctx.getResource("classpath:" + filename).getInputStream(), filename); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/FileService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.fileservice.mongodb; 17 | 18 | import static org.springframework.data.mongodb.core.query.Query.*; 19 | import static org.springframework.data.mongodb.gridfs.GridFsCriteria.*; 20 | 21 | import java.io.InputStream; 22 | 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.data.mongodb.gridfs.GridFsResource; 25 | import org.springframework.data.mongodb.gridfs.GridFsTemplate; 26 | import org.springframework.http.MediaType; 27 | import org.springframework.stereotype.Service; 28 | 29 | /** 30 | * @author Greg Turnquist 31 | */ 32 | @Service 33 | public class FileService { 34 | 35 | private final GridFsTemplate gridFsTemplate; 36 | 37 | @Autowired 38 | public FileService(GridFsTemplate gridFsTemplate) { 39 | 40 | this.gridFsTemplate = gridFsTemplate; 41 | } 42 | 43 | public void saveFile(InputStream input, String filename) { 44 | 45 | this.gridFsTemplate.delete(query(whereFilename().is(filename))); 46 | this.gridFsTemplate.store(input, filename, MediaType.IMAGE_JPEG_VALUE); 47 | } 48 | 49 | public GridFsResource[] findAll() { 50 | return this.gridFsTemplate.getResources("*"); 51 | } 52 | 53 | public GridFsResource findOne(String filename) { 54 | return this.gridFsTemplate.getResource(filename); 55 | } 56 | 57 | public void deleteAll() { 58 | 59 | for (GridFsResource resource : this.gridFsTemplate.getResources("*")) { 60 | this.deleteOne(resource.getFilename()); 61 | } 62 | } 63 | 64 | public void deleteOne(String filename) { 65 | this.gridFsTemplate.delete(query(whereFilename().is(filename))); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/FileServiceResourceProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.fileservice.mongodb; 17 | 18 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; 19 | 20 | import org.springframework.data.rest.webmvc.RepositoryLinksResource; 21 | import org.springframework.hateoas.ResourceProcessor; 22 | import org.springframework.stereotype.Component; 23 | 24 | /** 25 | * Add an extra root link to the controller that lists all files. 26 | * 27 | * @author Greg Turnquist 28 | */ 29 | @Component 30 | public class FileServiceResourceProcessor implements ResourceProcessor { 31 | 32 | @Override 33 | public RepositoryLinksResource process(RepositoryLinksResource resources) { 34 | 35 | resources.add(linkTo(methodOn(ApplicationController.class).listAllFiles()).withRel("files")); 36 | 37 | return resources; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/MongoDbFileServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.mongodb; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @SpringBootApplication 8 | @EnableDiscoveryClient 9 | public class MongoDbFileServiceApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(MongoDbFileServiceApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/RedisConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.fileservice.mongodb; 17 | 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.annotation.Configuration; 20 | import org.springframework.context.annotation.Profile; 21 | import org.springframework.session.data.redis.config.ConfigureRedisAction; 22 | 23 | /** 24 | * @author Greg Turnquist 25 | */ 26 | @Configuration 27 | public class RedisConfig { 28 | 29 | @Bean 30 | @Profile("cloud") 31 | public static ConfigureRedisAction configureRedisAction() { 32 | return ConfigureRedisAction.NO_OP; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/SecurityDetailsLoader.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.mongodb; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.annotation.PostConstruct; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | public class SecurityDetailsLoader { 13 | 14 | private final UserRepository userRepository; 15 | 16 | @Autowired 17 | public SecurityDetailsLoader(UserRepository userRepository) { 18 | this.userRepository = userRepository; 19 | } 20 | 21 | @PostConstruct 22 | public void init() throws IOException { 23 | 24 | userRepository.deleteAll(); 25 | 26 | User greg = new User(); 27 | greg.setName("greg"); 28 | greg.setPassword("turnquist"); 29 | greg.setRoles(new String[]{"ROLE_USER"}); 30 | userRepository.save(greg); 31 | 32 | User roy = new User(); 33 | roy.setName("roy"); 34 | roy.setPassword("clarkson"); 35 | roy.setRoles(new String[]{"ROLE_USER"}); 36 | userRepository.save(roy); 37 | 38 | SecurityContextHolder.clearContext(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/SpringDataMongoUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.mongodb; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.core.authority.AuthorityUtils; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class SpringDataMongoUserDetailsService implements UserDetailsService { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(SpringDataMongoUserDetailsService.class); 17 | 18 | private UserRepository repository; 19 | 20 | @Autowired 21 | public SpringDataMongoUserDetailsService(UserRepository repository) { 22 | this.repository = repository; 23 | } 24 | 25 | @Override 26 | public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { 27 | log.info("Fetching user " + s); 28 | User user = repository.findByName(s); 29 | log.info("Transforming " + user + " into UserDetails object"); 30 | UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(), 31 | AuthorityUtils.createAuthorityList(user.getRoles())); 32 | log.info("About to return " + userDetails); 33 | return userDetails; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/User.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.mongodb; 2 | 3 | import java.io.Serializable; 4 | 5 | import lombok.Data; 6 | import lombok.ToString; 7 | 8 | import org.springframework.data.annotation.Id; 9 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 10 | import org.springframework.security.crypto.password.PasswordEncoder; 11 | 12 | import com.fasterxml.jackson.annotation.JsonIgnore; 13 | 14 | @Data 15 | @ToString(exclude = "password") 16 | public class User implements Serializable { 17 | 18 | public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); 19 | 20 | @Id 21 | private String id; 22 | 23 | private String name; 24 | 25 | // tag::user[] 26 | // This field MUST be protected against any form of 27 | // serialization to avoid security leakage 28 | @JsonIgnore 29 | private String password; 30 | //end::user[] 31 | 32 | private String[] roles; 33 | 34 | public void setPassword(String password) { 35 | this.password = PASSWORD_ENCODER.encode(password); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.mongodb; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | 6 | @RepositoryRestResource(exported = false) 7 | public interface UserRepository extends CrudRepository { 8 | 9 | User findByName(String name); 10 | } 11 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/mongodb/WebSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.mongodb; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.core.env.Environment; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 9 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 13 | import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableGlobalMethodSecurity(prePostEnabled = true) 18 | @EnableRedisHttpSession 19 | public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { 20 | 21 | @Autowired 22 | SpringDataMongoUserDetailsService userDetailsService; 23 | 24 | @Autowired 25 | Environment env; 26 | 27 | @Override 28 | public void configure(AuthenticationManagerBuilder auth) throws Exception { 29 | auth.userDetailsService(userDetailsService).passwordEncoder(User.PASSWORD_ENCODER); 30 | } 31 | 32 | // Needed by Spring Security OAuth 33 | @Override 34 | @Bean 35 | public AuthenticationManager authenticationManagerBean() throws Exception { 36 | return super.authenticationManagerBean(); 37 | } 38 | 39 | @Override 40 | protected void configure(HttpSecurity http) throws Exception { 41 | http 42 | .authorizeRequests() 43 | // NOTE: If you add other static resources to src/main/resources, they must be 44 | // listed here to avoid security checks 45 | .antMatchers("/docs/**").permitAll() 46 | .anyRequest().authenticated() 47 | .and() 48 | .httpBasic() 49 | .and() 50 | .csrf().disable(); 51 | 52 | if (env.acceptsProfiles("ssl")) { 53 | http.requiresChannel().anyRequest().requiresSecure(); 54 | } 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: spring-a-gram-mongodb-fileservice 4 | 5 | multipart: 6 | maxFileSize: 10Mb 7 | 8 | server: 9 | port: 0 # Spring Boot randomly assigns a port number 10 | 11 | eureka: 12 | client: 13 | serviceUrl: 14 | defaultZone: ${eureka.address:localhost:8761}/eureka/ 15 | instance: 16 | leaseRenewalIntervalInSeconds: 5 17 | hostname: ${vcap.application.uris[0]:localhost} 18 | metadataMap: 19 | instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}} 20 | 21 | logging: 22 | level: 23 | ROOT: INFO 24 | com.gregturnquist.springagram: DEBUG 25 | 26 | --- 27 | spring: 28 | profiles: cloud 29 | 30 | eureka: 31 | instance: 32 | nonSecurePort: 80 33 | -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/resources/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregturn/spring-a-gram/171f8675ed22308936e43eebe4f18ef6d90d9010/spring-a-gram-mongodb-fileservice/src/main/resources/cat.jpg -------------------------------------------------------------------------------- /spring-a-gram-mongodb-fileservice/src/main/resources/caterpillar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregturn/spring-a-gram/171f8675ed22308936e43eebe4f18ef6d90d9010/spring-a-gram-mongodb-fileservice/src/main/resources/caterpillar.jpg -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.greglturnquist.springagram 9 | spring-a-gram 10 | 0.1.0 11 | 12 | 13 | com.greglturnquist.springagram 14 | spring-a-gram-s3-fileservice 15 | 0.1.0 16 | 17 | 18 | 1.1.0.BUILD-SNAPSHOT 19 | 20 | 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-aws-autoconfigure 25 | ${spring-cloud-aws.version} 26 | 27 | 28 | org.springframework.cloud 29 | spring-cloud-aws-context 30 | ${spring-cloud-aws.version} 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-data-jpa 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-rest 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-web 43 | 44 | 45 | org.springframework.hateoas 46 | spring-hateoas 47 | 48 | 49 | io.pivotal.spring.cloud 50 | spring-cloud-services-starter-service-registry 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-security 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/ApplicationController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.fileservice.s3; 17 | 18 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; 19 | 20 | import java.io.IOException; 21 | import java.net.URI; 22 | import java.net.URISyntaxException; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import org.springframework.beans.factory.annotation.Autowired; 28 | import org.springframework.context.annotation.Configuration; 29 | import org.springframework.core.io.InputStreamResource; 30 | import org.springframework.core.io.Resource; 31 | import org.springframework.hateoas.Link; 32 | import org.springframework.hateoas.ResourceSupport; 33 | import org.springframework.http.MediaType; 34 | import org.springframework.http.ResponseEntity; 35 | import org.springframework.web.bind.annotation.PathVariable; 36 | import org.springframework.web.bind.annotation.RequestMapping; 37 | import org.springframework.web.bind.annotation.RequestMethod; 38 | import org.springframework.web.bind.annotation.RequestParam; 39 | import org.springframework.web.bind.annotation.RestController; 40 | import org.springframework.web.multipart.MultipartFile; 41 | import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; 42 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 43 | 44 | /** 45 | * @author Greg Turnquist 46 | */ 47 | @RestController 48 | public class ApplicationController { 49 | 50 | private static final Logger log = LoggerFactory.getLogger(ApplicationController.class); 51 | 52 | private final FileService fileService; 53 | 54 | @Autowired 55 | public ApplicationController(FileService fileService) { 56 | this.fileService = fileService; 57 | } 58 | 59 | @RequestMapping(method = RequestMethod.POST, value = "/files") 60 | public ResponseEntity newFile(@RequestParam("name") String filename, @RequestParam("file") MultipartFile file) { 61 | 62 | try { 63 | this.fileService.saveFile(file.getInputStream(), file.getSize(), filename); 64 | 65 | Link link = linkTo(methodOn(ApplicationController.class).getFile(filename)).withRel(filename); 66 | return ResponseEntity.created(new URI(link.getHref())).build(); 67 | 68 | } catch (IOException | URISyntaxException e) { 69 | return ResponseEntity.badRequest().body("Couldn't process the request"); 70 | } 71 | } 72 | 73 | @RequestMapping(method = RequestMethod.GET, value = "/files") 74 | public ResponseEntity listFiles() { 75 | 76 | try { 77 | Resource[] files = this.fileService.findAll(); 78 | 79 | ResourceSupport resources = new ResourceSupport(); 80 | 81 | for (Resource file : files) { 82 | resources.add(linkTo(methodOn(ApplicationController.class).getFile(file.getFilename())) 83 | .withRel(file.getFilename())); 84 | } 85 | 86 | return ResponseEntity.ok(resources); 87 | } catch (IOException e) { 88 | return ResponseEntity.badRequest().body(e.getMessage()); 89 | } 90 | } 91 | 92 | @RequestMapping(method = RequestMethod.GET, value = "/files/{filename}") 93 | public ResponseEntity getFile(@PathVariable String filename) throws IOException { 94 | 95 | Resource file = this.fileService.findOne(filename); 96 | 97 | try { 98 | return ResponseEntity.ok().contentLength(file.contentLength()) 99 | .contentType(MediaType.IMAGE_JPEG) 100 | .body(new InputStreamResource(file.getInputStream())); 101 | } 102 | catch (IOException e) { 103 | return ResponseEntity.badRequest().body("Couldn't process the request"); 104 | } 105 | } 106 | 107 | @RequestMapping(method = RequestMethod.DELETE, value = "/files/{filename}") 108 | public ResponseEntity deleteFile(@PathVariable String filename) { 109 | 110 | this.fileService.deleteOne(filename); 111 | 112 | return ResponseEntity.noContent().build(); 113 | } 114 | 115 | @Configuration 116 | static class AllResources extends WebMvcConfigurerAdapter { 117 | 118 | @Override 119 | public void configurePathMatch(PathMatchConfigurer matcher) { 120 | matcher.setUseSuffixPatternMatch(false); 121 | } 122 | 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/DatabaseLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.fileservice.s3; 17 | 18 | import java.io.IOException; 19 | 20 | import javax.annotation.PostConstruct; 21 | 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.context.ApplicationContext; 24 | import org.springframework.context.annotation.Profile; 25 | import org.springframework.core.io.Resource; 26 | import org.springframework.stereotype.Service; 27 | 28 | /** 29 | * @author Greg Turnquist 30 | */ 31 | @Service 32 | @Profile("!production") 33 | public class DatabaseLoader { 34 | 35 | private final FileService fileService; 36 | private final ApplicationContext ctx; 37 | 38 | @Autowired 39 | public DatabaseLoader(FileService fileService, ApplicationContext ctx) { 40 | this.fileService = fileService; 41 | this.ctx = ctx; 42 | } 43 | 44 | @PostConstruct 45 | public void init() throws IOException { 46 | 47 | this.fileService.deleteAll(); 48 | 49 | // loadImage("cat.jpg"); 50 | // loadImage("caterpillar.jpg"); 51 | } 52 | 53 | private void loadImage(String filename) throws IOException { 54 | Resource resource = ctx.getResource("classpath:" + filename); 55 | this.fileService.saveFile(resource.getInputStream(), resource.getFile().length(), filename); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/FileService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.fileservice.s3; 17 | 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | 21 | import javax.xml.parsers.DocumentBuilderFactory; 22 | import javax.xml.xpath.XPathExpression; 23 | import javax.xml.xpath.XPathFactory; 24 | 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | import org.w3c.dom.Document; 28 | 29 | import org.springframework.beans.factory.annotation.Autowired; 30 | import org.springframework.beans.factory.annotation.Value; 31 | import org.springframework.core.io.Resource; 32 | import org.springframework.core.io.ResourceLoader; 33 | import org.springframework.core.io.support.ResourcePatternResolver; 34 | import org.springframework.stereotype.Service; 35 | 36 | import com.amazonaws.services.s3.AmazonS3Client; 37 | import com.amazonaws.services.s3.model.AmazonS3Exception; 38 | import com.amazonaws.services.s3.model.ObjectMetadata; 39 | import com.amazonaws.util.StringInputStream; 40 | 41 | /** 42 | * @author Greg Turnquist 43 | */ 44 | @Service 45 | public class FileService { 46 | 47 | private static final Logger log = LoggerFactory.getLogger(FileService.class); 48 | 49 | private final ResourcePatternResolver resourcePatternResolver; 50 | private final ResourceLoader resourceLoader; 51 | private final AmazonS3Client s3Client; 52 | 53 | @Value("${bucket}") 54 | private String bucket; 55 | 56 | @Autowired 57 | public FileService(ResourcePatternResolver resourcePatternResolver, 58 | ResourceLoader resourceLoader, AmazonS3Client s3Client) { 59 | 60 | this.resourcePatternResolver = resourcePatternResolver; 61 | this.resourceLoader = resourceLoader; 62 | this.s3Client = s3Client; 63 | } 64 | 65 | public void saveFile(InputStream input, long length, String filename) throws IOException { 66 | 67 | try { 68 | ObjectMetadata metadata = new ObjectMetadata(); 69 | metadata.setContentLength(length); 70 | this.s3Client.putObject(this.bucket, filename, input, metadata); 71 | } 72 | catch (AmazonS3Exception e) { 73 | if (e.getStatusCode() == 301) { 74 | updateEndpoint(e); 75 | saveFile(input, length, filename); 76 | } 77 | } 78 | } 79 | 80 | public Resource[] findAll() throws IOException { 81 | 82 | Resource[] results = new Resource[0]; 83 | try { 84 | results = this.resourcePatternResolver.getResources(s3ify(this.bucket) + "/" + "*"); 85 | } 86 | catch (AmazonS3Exception e) { 87 | if (e.getStatusCode() == 301) { 88 | updateEndpoint(e); 89 | results = this.findAll(); 90 | } 91 | } 92 | return results; 93 | } 94 | 95 | public Resource findOne(String filename) { 96 | return this.resourceLoader.getResource(s3ify(this.bucket) + "/" + filename); 97 | } 98 | 99 | public void deleteAll() throws IOException { 100 | 101 | for (Resource resource : this.findAll()) { 102 | this.deleteOne(resource.getFilename()); 103 | } 104 | } 105 | 106 | public void deleteOne(String filename) { 107 | this.s3Client.deleteObject(this.bucket, filename); 108 | } 109 | 110 | private String s3ify(String s) { 111 | if (s.startsWith("s3://")) { 112 | return s; 113 | } else { 114 | return "s3://" + s; 115 | } 116 | } 117 | 118 | /** 119 | * Parse the {@link AmazonS3Exception} error result to capture the endpoint for 120 | * redirection. 121 | * 122 | * @param e 123 | */ 124 | private void updateEndpoint(AmazonS3Exception e) { 125 | 126 | try { 127 | Document errorResponseDoc = DocumentBuilderFactory 128 | .newInstance() 129 | .newDocumentBuilder() 130 | .parse(new StringInputStream(e.getErrorResponseXml())); 131 | 132 | XPathExpression endpointXpathExtr = XPathFactory.newInstance().newXPath().compile("/Error/Endpoint"); 133 | 134 | this.s3Client.setEndpoint(endpointXpathExtr.evaluate(errorResponseDoc)); 135 | } 136 | catch (Exception ex) { 137 | throw new RuntimeException(e); 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/FileServiceResourceProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.fileservice.s3; 17 | 18 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; 19 | 20 | import org.springframework.data.rest.webmvc.RepositoryLinksResource; 21 | import org.springframework.hateoas.ResourceProcessor; 22 | import org.springframework.stereotype.Component; 23 | 24 | /** 25 | * Add an extra root link to the controller that lists all files. 26 | * 27 | * @author Greg Turnquist 28 | */ 29 | @Component 30 | public class FileServiceResourceProcessor implements ResourceProcessor { 31 | 32 | @Override 33 | public RepositoryLinksResource process(RepositoryLinksResource resources) { 34 | 35 | resources.add(linkTo(methodOn(ApplicationController.class).listFiles()).withRel("files")); 36 | 37 | return resources; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/RedisConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.greglturnquist.springagram.fileservice.s3; 17 | 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.annotation.Configuration; 20 | import org.springframework.context.annotation.Profile; 21 | import org.springframework.session.data.redis.config.ConfigureRedisAction; 22 | 23 | /** 24 | * @author Greg Turnquist 25 | */ 26 | @Configuration 27 | public class RedisConfig { 28 | 29 | @Bean 30 | @Profile("cloud") 31 | public static ConfigureRedisAction configureRedisAction() { 32 | return ConfigureRedisAction.NO_OP; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/S3FileServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.s3; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.EnableEurekaClient; 6 | 7 | @SpringBootApplication 8 | @EnableEurekaClient 9 | public class S3FileServiceApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(S3FileServiceApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/SecurityDetailsLoader.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.s3; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.annotation.PostConstruct; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | public class SecurityDetailsLoader { 13 | 14 | private final UserRepository userRepository; 15 | 16 | @Autowired 17 | public SecurityDetailsLoader(UserRepository userRepository) { 18 | this.userRepository = userRepository; 19 | } 20 | 21 | @PostConstruct 22 | public void init() throws IOException { 23 | 24 | userRepository.deleteAll(); 25 | 26 | User greg = new User(); 27 | greg.setName("greg"); 28 | greg.setPassword("turnquist"); 29 | greg.setRoles(new String[]{"ROLE_USER"}); 30 | userRepository.save(greg); 31 | 32 | User roy = new User(); 33 | roy.setName("roy"); 34 | roy.setPassword("clarkson"); 35 | roy.setRoles(new String[]{"ROLE_USER"}); 36 | userRepository.save(roy); 37 | 38 | SecurityContextHolder.clearContext(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/SpringDataJpaUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.s3; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.core.authority.AuthorityUtils; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class SpringDataJpaUserDetailsService implements UserDetailsService { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(SpringDataJpaUserDetailsService.class); 17 | 18 | private UserRepository repository; 19 | 20 | @Autowired 21 | public SpringDataJpaUserDetailsService(UserRepository repository) { 22 | this.repository = repository; 23 | } 24 | 25 | @Override 26 | public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { 27 | log.info("Fetching user " + s); 28 | User user = repository.findByName(s); 29 | log.info("Transforming " + user + " into UserDetails object"); 30 | UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(), 31 | AuthorityUtils.createAuthorityList(user.getRoles())); 32 | log.info("About to return " + userDetails); 33 | return userDetails; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/User.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.s3; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.Id; 8 | 9 | import lombok.Data; 10 | import lombok.ToString; 11 | 12 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | 15 | import com.fasterxml.jackson.annotation.JsonIgnore; 16 | 17 | @Entity 18 | @Data 19 | @ToString(exclude = "password") 20 | public class User implements Serializable { 21 | 22 | public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); 23 | 24 | @Id @GeneratedValue 25 | private long id; 26 | 27 | private String name; 28 | 29 | // tag::user[] 30 | // This field MUST be protected against any form of 31 | // serialization to avoid security leakage 32 | @JsonIgnore 33 | private String password; 34 | //end::user[] 35 | 36 | private String[] roles; 37 | 38 | public void setPassword(String password) { 39 | this.password = PASSWORD_ENCODER.encode(password); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.s3; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | 6 | @RepositoryRestResource(exported = false) 7 | public interface UserRepository extends CrudRepository { 8 | 9 | User findByName(String name); 10 | } 11 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/java/com/greglturnquist/springagram/fileservice/s3/WebSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.greglturnquist.springagram.fileservice.s3; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.core.env.Environment; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 9 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 13 | import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableGlobalMethodSecurity(prePostEnabled = true) 18 | @EnableRedisHttpSession 19 | public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { 20 | 21 | @Autowired 22 | SpringDataJpaUserDetailsService userDetailsService; 23 | 24 | @Autowired 25 | Environment env; 26 | 27 | @Override 28 | public void configure(AuthenticationManagerBuilder auth) throws Exception { 29 | auth.userDetailsService(userDetailsService).passwordEncoder(User.PASSWORD_ENCODER); 30 | } 31 | 32 | // Needed by Spring Security OAuth 33 | @Override 34 | @Bean 35 | public AuthenticationManager authenticationManagerBean() throws Exception { 36 | return super.authenticationManagerBean(); 37 | } 38 | 39 | @Override 40 | protected void configure(HttpSecurity http) throws Exception { 41 | http 42 | .authorizeRequests() 43 | // NOTE: If you add other static resources to src/main/resources, they must be 44 | // listed here to avoid security checks 45 | .antMatchers("/docs/**").permitAll() 46 | .anyRequest().authenticated() 47 | .and() 48 | .httpBasic() 49 | .and() 50 | .csrf().disable(); 51 | 52 | if (env.acceptsProfiles("ssl")) { 53 | http.requiresChannel().anyRequest().requiresSecure(); 54 | } 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: spring-a-gram-s3-fileservice 4 | 5 | multipart: 6 | maxFileSize: 10Mb 7 | 8 | server: 9 | port: 0 # Spring Boot randomly assigns a port number 10 | 11 | eureka: 12 | client: 13 | serviceUrl: 14 | defaultZone: ${eureka.address:localhost:8761}/eureka/ 15 | instance: 16 | leaseRenewalIntervalInSeconds: 5 17 | hostname: ${vcap.application.uris[0]:localhost} 18 | metadataMap: 19 | instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}} 20 | 21 | logging: 22 | level: 23 | ROOT: INFO 24 | com.gregturnquist.springagram: DEBUG 25 | 26 | --- 27 | spring: 28 | profiles: cloud 29 | 30 | eureka: 31 | instance: 32 | nonSecurePort: 80 33 | -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/resources/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregturn/spring-a-gram/171f8675ed22308936e43eebe4f18ef6d90d9010/spring-a-gram-s3-fileservice/src/main/resources/cat.jpg -------------------------------------------------------------------------------- /spring-a-gram-s3-fileservice/src/main/resources/caterpillar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregturn/spring-a-gram/171f8675ed22308936e43eebe4f18ef6d90d9010/spring-a-gram-s3-fileservice/src/main/resources/caterpillar.jpg -------------------------------------------------------------------------------- /src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | VOLUME /tmp 3 | ADD spring-a-gram-0.1.0.jar app.jar 4 | RUN bash -c 'touch /app.jar' 5 | ENTRYPOINT ["java","-jar","/app.jar"] -------------------------------------------------------------------------------- /swagger/app.groovy: -------------------------------------------------------------------------------- 1 | import java.io.IOException 2 | import javax.servlet.Filter 3 | import javax.servlet.FilterChain 4 | import javax.servlet.FilterConfig 5 | import javax.servlet.ServletException 6 | import javax.servlet.ServletRequest 7 | import javax.servlet.ServletResponse 8 | import javax.servlet.http.HttpServletResponse 9 | import org.springframework.stereotype.Component 10 | 11 | @Controller 12 | class JsApp { } 13 | 14 | @Component 15 | class SimpleCORSFilter implements Filter { 16 | 17 | void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { 18 | HttpServletResponse response = (HttpServletResponse) res 19 | response.setHeader("Access-Control-Allow-Origin", "*") 20 | response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") 21 | response.setHeader("Access-Control-Max-Age", "3600") 22 | response.setHeader("Access-Control-Allow-Headers", "x-requested-with") 23 | chain.doFilter(req, res) 24 | } 25 | 26 | void init(FilterConfig filterConfig) {} 27 | 28 | void destroy() {} 29 | 30 | } -------------------------------------------------------------------------------- /swagger/clean.js: -------------------------------------------------------------------------------- 1 | var swagger = { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "These are all the links for form controls from an Amazon web page", 5 | "version": "1.0.0", 6 | "title": "Amazon" 7 | }, 8 | "paths": { 9 | 10 | } 11 | }; 12 | 13 | var links = document.querySelectorAll('a'); 14 | for (var i = 0; i < links.length; i++) { 15 | var link = links[i].href; 16 | var path = "/" + link.split('/').slice(3,-1).join('/') 17 | var stuff = { 18 | "description" : link, 19 | "responses": { 20 | "200": { 21 | "description": "success" 22 | } 23 | } 24 | }; 25 | if (i % 2 === 0) { 26 | swagger.paths[path] = { 27 | "get": stuff 28 | } 29 | } else { 30 | swagger.paths[path] = { 31 | "post": stuff 32 | } 33 | } 34 | links[i].remove(); 35 | } 36 | var forms = document.querySelectorAll('form'); 37 | for (var i = 0; i < forms.length; i++) { 38 | forms[i].remove(); 39 | } 40 | console.log(JSON.stringify(swagger, null, 2)); -------------------------------------------------------------------------------- /swagger/static/amazon.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "These are all the links for form controls from an Amazon web page", 5 | "version": "1.0.0", 6 | "title": "Amazon" 7 | }, 8 | "host": "amazon.com", 9 | "paths": { 10 | "/": { 11 | "get": { 12 | "description": "http://amazon.com/ref=nav_logo", 13 | "responses": { 14 | "200": { 15 | "description": "Success" 16 | } 17 | } 18 | } 19 | }, 20 | "/gp/product/B00DBYBNEE": { 21 | "post": { 22 | "description": "http://amazon.com//gp/product/B00DBYBNEE/ref=nav_joinprmlogo", 23 | "responses": { 24 | "200": { 25 | "description" : "Success" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } --------------------------------------------------------------------------------